From 3ea1847ba32fba68120cd5d874187ad814213b0b Mon Sep 17 00:00:00 2001 From: devAsmodeus Date: Sun, 3 May 2026 23:58:10 +0300 Subject: [PATCH 1/4] =?UTF-8?q?fix(solo):=20prev-hash=20byte-order=20BLOCK?= =?UTF-8?q?ER=20B1=20=E2=80=94=20internal=20LE=20in=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_template_to_job` собирал prevhash в Stratum-style как `BE_chunked_per_4`, что после `swap_words` в `_build_header_base` сокращалось в no-op и оставляло display BE на месте prev_hash. Submit-time `_assemble_header` независимо делает BE→LE, поэтому hashing-time и submit-time заголовки расходились — на реальном bitcoind solo-mode тихо подписывал бы невалидные блоки. Фикс: считаем `prev_internal_le = prev_be[::-1]`, затем per-4-byte reverse, чтобы `swap_words(prev_stratum_hex) == prev_internal_le`. Маскировал баг тестовый `FAKE_TEMPLATE.previousblockhash = "0"*64` — симметричный фикспоинт под любой байт-перестановкой. Добавлены два sentinel-теста с реальным mainnet prev-hash (block #800000): `test_template_to_job_prevhash_internal_le_after_swap_words` и `test_build_header_base_uses_internal_le_prevhash`. Tests: 242 → 244. Refs: docs/handoff/final-review.md B1. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hope_hash/solo.py | 28 ++++++++++++++----------- tests/test_solo.py | 48 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/hope_hash/solo.py b/src/hope_hash/solo.py index 1254af6..1d80333 100644 --- a/src/hope_hash/solo.py +++ b/src/hope_hash/solo.py @@ -504,20 +504,24 @@ def _template_to_job(self, tmpl: dict[str, Any], job_id: str) -> dict[str, Any]: txids = [bytes.fromhex(tx["txid"])[::-1] for tx in tmpl.get("transactions", [])] branch = self._merkle_branch_from_txids(txids) - # Stratum-style формат: prevhash word-swap'ed; в solo-режиме мы - # шаблон отдаём «как есть», а в `_build_header_base` всё равно - # будет swap_words применён → значит здесь нужен ОБРАТНЫЙ swap, - # чтобы compose был корректным. Делаем raw prevhash и в miner.py - # _build_header_base вызовет swap_words → получим как надо. - # bitcoind отдаёт previousblockhash big-endian hex (display), нам - # нужен LE для header. swap_words конвертирует «stratum-формат» - # обратно в LE; чтобы совпало, передадим prevhash в stratum-формате. + # bitcoind отдаёт previousblockhash в display (big-endian) форме, + # а в block header нужен internal little-endian (BE[::-1]). + # `_build_header_base` применяет к нашему prevhash `swap_words`, + # которая разворачивает каждую 4-байтную группу. Чтобы после + # swap_words получился internal LE, передаём internal LE с + # пред-развёрнутыми 4-байтными группами — два разворота + # сократятся, и в header ляжет правильная LE. + # + # Маскировалось до фикса: тестовый FAKE_TEMPLATE.previousblockhash + # = "0"*64 — симметричный фикспоинт под любой перестановкой + # байт. На реальном bitcoind мы бы тихо подписывали невалидные + # шары: hashing-time header содержал бы display BE вместо LE, + # а submit-time _assemble_header независимо делает BE→LE и + # отдавал бы другой header. См. docs/handoff/final-review.md B1. prev_be = bytes.fromhex(tmpl["previousblockhash"]) - # Stratum prev = big-endian с word-swap'ом по 4 байта. - # Inverse: байты берём как есть (display=BE), а внутри 4-байтных - # групп переставляем little-endian → swap_words восстановит BE. + prev_internal_le = prev_be[::-1] prev_stratum_hex = b"".join( - prev_be[i:i+4][::-1] for i in range(0, 32, 4) + prev_internal_le[i:i+4][::-1] for i in range(0, 32, 4) ).hex() # version/bits/curtime приходят как int → конвертируем в hex BE-строку, diff --git a/tests/test_solo.py b/tests/test_solo.py index 8bf08ca..e1468f7 100644 --- a/tests/test_solo.py +++ b/tests/test_solo.py @@ -7,7 +7,8 @@ import unittest from unittest.mock import patch -from hope_hash.block import double_sha256 +from hope_hash.block import double_sha256, swap_words +from hope_hash.miner import _build_header_base from hope_hash.solo import ( BitcoinRPC, RPCError, @@ -354,6 +355,51 @@ def test_submit_reject_calls_callback_with_false(self): self.assertEqual(len(results), 1) self.assertFalse(results[0][1]) + def test_template_to_job_prevhash_internal_le_after_swap_words(self): + # B1 regression sentinel — final-review.md. + # Real mainnet block #800000 prev hash (display BE): + # bitcoind отдаёт previousblockhash в display-форме; в block header + # должна лежать internal LE (= display BE, развёрнутый побайтно). + # _build_header_base применяет swap_words к prevhash из job-словаря, + # так что после swap_words мы обязаны получить ровно prev_be[::-1]. + real_prev_be_hex = ( + "00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054" + ) + prev_be = bytes.fromhex(real_prev_be_hex) + prev_internal_le = prev_be[::-1] + + # Symmetric fixture скрывал бы баг: убеждаемся что prevhash вообще + # несимметричный (любой no-op-фикс на нём провалится). + self.assertNotEqual(prev_be, prev_internal_le) + + tmpl = dict(FAKE_TEMPLATE, previousblockhash=real_prev_be_hex) + client = self._make_client(rpc=FakeRPC(template=tmpl)) + client.connect() + + job = client.current_job + self.assertEqual(swap_words(job["prevhash"]), prev_internal_le) + + def test_build_header_base_uses_internal_le_prevhash(self): + # Сквозной чек: hashing-time header (mining) и submit-time header + # (_assemble_header) должны видеть одинаковый prev_hash. + # До B1-фикса miner получал display BE, submitter — internal LE, + # и они расходились. + real_prev_be_hex = ( + "00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054" + ) + prev_internal_le = bytes.fromhex(real_prev_be_hex)[::-1] + + tmpl = dict(FAKE_TEMPLATE, previousblockhash=real_prev_be_hex) + client = self._make_client(rpc=FakeRPC(template=tmpl)) + client.connect() + + header_base = _build_header_base( + client.current_job, extranonce1="", extranonce2="00000000" + ) + # Layout: 4b version | 32b prev_hash | 32b merkle | 4b ntime | 4b nbits + prev_in_header = header_base[4:36] + self.assertEqual(prev_in_header, prev_internal_le) + class TestBitcoinRPCAuth(unittest.TestCase): def test_no_auth_raises(self): From 25c0ae49d4414c620e6d48af668290b906b11ae8 Mon Sep 17 00:00:00 2001 From: devAsmodeus Date: Sun, 3 May 2026 23:58:21 +0300 Subject: [PATCH 2/4] docs: address final-review S1-S4 should-fix items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S1: docker-compose.yml header — :8001 is the web dashboard, :8000 hosts /metrics + /healthz (was misnamed, sent first-run users to the Prometheus exposition page). - S2: docs/deploy.{en,ru}.md §6 docker run example — replace literal `docker` positional with `mybox`; the arg is `worker_name` for the pool, not a subcommand. Added inline comment. - S3: CHANGELOG.md link footer — restore compare-links for v0.3.0 through v0.7.0 and re-anchor [Unreleased] against v0.7.0 (was stuck at v0.2.0 since the 0.3.0 release). - S4: README.md advanced flags table (EN + RU halves) — add `--log-file PATH`, which lives in cli.py and is used in the TUI workflow but was missing from the table. Refs: docs/handoff/final-review.md S1-S4. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 56 +++++++++++++++++++++++++++++++++++++++++++++- README.md | 2 ++ docker-compose.yml | 6 +++-- docs/deploy.en.md | 5 ++++- docs/deploy.ru.md | 5 ++++- 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feeac8..fda54ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,55 @@ ## [Unreleased] +### Исправлено +- **`solo.py:_template_to_job` — prev-hash byte-order BLOCKER.** + Stratum-style формат для `prevhash` собирался из display-BE с + word-swap'ом, что после применения `swap_words` в `_build_header_base` + сокращалось в no-op и оставляло display-BE в hashing-time header'е. + Submit-time `_assemble_header` независимо делает `BE→LE`, поэтому + заголовки для майнинга и для submit'а расходились — на реальном + bitcoind solo-режим тихо производил бы невалидные блоки. Теперь: + `prev_internal_le = prev_be[::-1]` → потом per-4-byte reverse, чтобы + `swap_words` дал ровно internal LE. Тестовый `FAKE_TEMPLATE` ранее + маскировал баг (`previousblockhash = "0"*64` — фикспоинт под любой + байт-перестановкой). +- **`docker-compose.yml` шапка** — указывала `:8000` для веб-дашборда, + тогда как фактически дашборд на `:8001`, а на `:8000` — Prometheus + exposition + healthz. Комментарий синхронизирован с реальностью. +- **`docs/deploy.{en,ru}.md` §6 `docker run` пример** — позиционник + `docker` читался как сабкоманда; заменён на `mybox` с пояснением, + что это просто `worker_name` для пула. +- **`CHANGELOG.md` link footer** — `[Unreleased]` сравнивался против + `v0.2.0`, версии 0.3.0–0.7.0 не имели compare-link'ов; восстановлено. +- **`README.md` advanced-flags table (EN + RU)** — добавлен `--log-file + PATH`, который реально живёт в `cli.py` и используется в TUI workflow. + +### Добавлено +- **Regression sentinels** для B1 в [tests/test_solo.py](tests/test_solo.py): + `test_template_to_job_prevhash_internal_le_after_swap_words` (real + mainnet block #800000 prev hash, проверка что `swap_words(job["prevhash"]) + == prev_be[::-1]`) и `test_build_header_base_uses_internal_le_prevhash` + (сквозной чек что hashing-time header содержит ровно internal LE + в позиции 4..36). Тестов: 242 → **244**. +- **release-please workflow** ([.github/workflows/release-please.yml](.github/workflows/release-please.yml)) + + [release-please-config.json](release-please-config.json) + + [.release-please-manifest.json](.release-please-manifest.json). + Теперь conventional-commit пуши в `main` автоматически открывают + release-PR с агрегированным CHANGELOG, бампом версии в + `pyproject.toml` и `__init__.py` (через `x-release-please-version` + маркеры), GitHub-релизом + тегом. PAT `RELEASE_PLEASE_TOKEN` + опционален: без него релиз-PR создаётся, но downstream-CI не + триггерится — нужен manual close+reopen. + +### Изменено +- **`pyproject.toml`** — статическая `version = "0.7.0"` вместо + `dynamic = ["version"]` (release-please ожидает статический поле для + in-place бампа). `[tool.hatch.version]` блок удалён. `__version__` в + `src/hope_hash/__init__.py` остаётся single source of truth для + runtime-кода (`from hope_hash import __version__`); release-please + держит обе строки в синхроне через `x-release-please-version` + comment-маркеры. + ## [0.7.0] — 2026-05-02 ### Добавлено @@ -254,6 +303,11 @@ - 15 юнит-тестов на криптографические функции (`unittest`). - Реструктуризация в `src/`-layout с пакетом `hope_hash`. -[Unreleased]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/devAsmodeus/Hope-Hash/releases/tag/v0.1.0 diff --git a/README.md b/README.md index ab177ba..6536477 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ the first network round-trip — typos fail fast with a precise message. | `--suggest-diff DIFF` | Send `mining.suggest_difficulty` after authorize (vardiff). | | `--demo` | Offline mode against a synthetic header. | | `--benchmark` | Hashrate microbenchmark, no networking. | +| `--log-file PATH` | Mirror logs to a file (handy with `--tui`, which clears stderr). | | `--no-banner` | Skip the ASCII banner (cron / systemd). | Full help: `hope-hash --help`. @@ -220,6 +221,7 @@ python -m hope_hash bc1q5n2x4pvxhq8sxc7ck3uxq8sxc7ck3uxqzfm2py mylaptop | `--suggest-diff DIFF` | Отправляет `mining.suggest_difficulty` после авторизации. | | `--demo` | Offline-режим с синтетическим заголовком. | | `--benchmark` | Микробенчмарк хешрейта без сети. | +| `--log-file PATH` | Дублировать логи в файл (полезно с `--tui`, которая зачищает stderr). | | `--no-banner` | Без ASCII-баннера (cron / systemd). | Полная справка: `hope-hash --help`. diff --git a/docker-compose.yml b/docker-compose.yml index d944e45..f23085f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,13 @@ # для Telegram-уведомлений. # # docker compose up -d -# open http://localhost:8000 # web-дашборд +# open http://localhost:8001 # web-дашборд (single-page UI) +# open http://localhost:8000/metrics # Prometheus exposition +# open http://localhost:8000/healthz # health JSON (200/503) # open http://localhost:3000 # Grafana (admin / admin при первом входе) # open http://localhost:9090 # Prometheus UI # -# Если порт 8000 занят — поменяйте маппинг здесь и у Prometheus в +# Если порт 8000 или 8001 занят — поменяйте маппинг здесь и у Prometheus в # deploy/prometheus/prometheus.yml. services: diff --git a/docs/deploy.en.md b/docs/deploy.en.md index 4887582..735616c 100644 --- a/docs/deploy.en.md +++ b/docs/deploy.en.md @@ -79,8 +79,11 @@ docker run -d --name hope-hash \ -p 8000:8000 -p 8001:8001 \ -v $(pwd)/data:/data \ hope-hash:0.7.0 \ - bc1q...your_address... docker --workers 2 \ + bc1q...your_address... mybox \ + --workers 2 \ --metrics-port 8000 --web-port 8001 --web-host 0.0.0.0 --no-banner +# positional args: — the literal "mybox" +# is just the worker_name shown to the pool; pick anything you like. ``` ## 7. Behind a reverse proxy diff --git a/docs/deploy.ru.md b/docs/deploy.ru.md index 875f6d7..862b17b 100644 --- a/docs/deploy.ru.md +++ b/docs/deploy.ru.md @@ -78,8 +78,11 @@ docker run -d --name hope-hash \ -p 8000:8000 -p 8001:8001 \ -v $(pwd)/data:/data \ hope-hash:0.7.0 \ - bc1q...твой_адрес... docker --workers 2 \ + bc1q...твой_адрес... mybox \ + --workers 2 \ --metrics-port 8000 --web-port 8001 --web-host 0.0.0.0 --no-banner +# позиционные аргументы: — литерал "mybox" +# здесь это просто имя воркера для пула; подставьте любое. ``` ## 7. За reverse-proxy From 35948f90e8bb39ad744edeba7a766e9407c2ad8e Mon Sep 17 00:00:00 2001 From: devAsmodeus Date: Sun, 3 May 2026 23:58:35 +0300 Subject: [PATCH 3/4] ci: add release-please workflow for automated releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the setup used in Aegis-Protocol: conventional-commit pushes to main now open an automated release-PR with aggregated CHANGELOG, version bumps and a GitHub release + tag once merged. - .github/workflows/release-please.yml — googleapis/release-please-action@v5 with release-type: python. Token resolution: RELEASE_PLEASE_TOKEN (fine-grained PAT, contents:write + pull-requests:write) preferred to trigger downstream CI on the release-PR; falls back to GITHUB_TOKEN (release-PR will need manual close+reopen to pick up CI). - release-please-config.json — single-package monorepo setup at root, bump-minor-pre-major: true so v0.x stays semver-pre-1.0 friendly, extra-files lists src/hope_hash/__init__.py for the __version__ bump. - .release-please-manifest.json — pins current version "0.7.0" so release-please knows where to start. - pyproject.toml — switch from `dynamic = ["version"]` (hatch-style) to static `version = "0.7.0"` so release-please can do an in-place bump. The legacy [tool.hatch.version] block is removed; hatch reads the static field. `__version__` in __init__.py stays the runtime SoT. - Both version strings carry a `# x-release-please-version` comment marker so release-please updates them deterministically. Refs: ROADMAP.md «Запланировано на v0.7.x / v0.8.0». Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release-please.yml | 35 ++++++++++++++++++++++++++++ .release-please-manifest.json | 3 +++ pyproject.toml | 8 ++++--- release-please-config.json | 19 +++++++++++++++ src/hope_hash/__init__.py | 2 +- 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..62f7290 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,35 @@ +name: Release Please + +# Why the custom token: +# +# release-please opens its release PR on every push to main. By default it +# uses the workflow-provided GITHUB_TOKEN; PRs created with that token do +# not trigger downstream workflows (GitHub Actions safety against recursion). +# Without a custom token, the release PR sits with no `CI` checks running +# and stays BLOCKED by branch protection. +# +# RELEASE_PLEASE_TOKEN is a fine-grained PAT (scope: contents:write, +# pull-requests:write on this repo) stored as a repo secret. PRs opened +# with it appear from the PAT owner's account and trigger CI normally. +# If the secret is absent, we fall back to GITHUB_TOKEN — the release PR +# will still be created but will require a manual close+reopen to trigger +# the required checks. + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v5 + with: + release-type: python + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e7ca613 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.7.0" +} diff --git a/pyproject.toml b/pyproject.toml index b1c2de0..99da6d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "hatchling.build" [project] name = "hope-hash" +version = "0.7.0" # x-release-please-version description = "Учебный solo BTC miner на чистом Python stdlib" readme = "README.md" requires-python = ">=3.11" @@ -25,7 +26,6 @@ classifiers = [ "Topic :: Security :: Cryptography", ] dependencies = [] -dynamic = ["version"] [project.urls] Repository = "https://github.com/devAsmodeus/Hope-Hash" @@ -34,8 +34,10 @@ Issues = "https://github.com/devAsmodeus/Hope-Hash/issues" [project.scripts] hope-hash = "hope_hash.cli:main" -[tool.hatch.version] -path = "src/hope_hash/__init__.py" +# Версия — двойной источник: статическая в [project].version и в +# `__init__.py.__version__`. release-please-action синхронизирует обе +# при бампе. Для рантайм-кода используется только __version__ (через +# `from hope_hash import __version__`). [tool.hatch.build.targets.wheel] packages = ["src/hope_hash"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..201fcf8 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "python", + "include-component-in-tag": false, + "include-v-in-tag": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "draft": false, + "prerelease": false, + "packages": { + ".": { + "package-name": "hope-hash", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "src/hope_hash/__init__.py" + ] + } + } +} diff --git a/src/hope_hash/__init__.py b/src/hope_hash/__init__.py index be1ab09..362d2e7 100644 --- a/src/hope_hash/__init__.py +++ b/src/hope_hash/__init__.py @@ -1,6 +1,6 @@ """Hope-Hash — учебный solo BTC miner на чистом stdlib.""" -__version__ = "0.7.0" +__version__ = "0.7.0" # x-release-please-version from . import sha_native from .banner import print_banner, render_banner From dbc7108d19244955cb26099a30d84f3196b0c286 Mon Sep 17 00:00:00 2001 From: devAsmodeus Date: Sun, 3 May 2026 23:58:44 +0300 Subject: [PATCH 4/4] docs(roadmap): defer M-1..M-4, test-gaps, nice-to-haves to v0.7.x/v0.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final-review left 4 MEDIUM security findings, 6 test-gaps and 11 docs nice-to-haves once B1 + S1-S4 land. They're not blockers for this PR-stack merge, but they need a home so they don't drift. - ROADMAP.md gets a new «Запланировано на v0.7.x / v0.8.0» section with every leftover item from final-review, grouped by category and tied back to file:line references where applicable. - docs/handoff/final-review.md gets a 2026-05-03 status banner noting B1 and S1-S4 are now closed, tests are 244, and pointing to ROADMAP for the rest. Co-Authored-By: Claude Opus 4.7 (1M context) --- ROADMAP.md | 74 ++++++++++++++++++++++++++++++++++++ docs/handoff/final-review.md | 8 ++++ 2 files changed, 82 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 517f480..5c21f1c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -122,6 +122,80 @@ TUI и команды Telegram — отложены. --- +## Запланировано на v0.7.x / v0.8.0 (post-v0.7.0 follow-up) + +Из консолидированного [final-review](docs/handoff/final-review.md) от +2026-05-02. **B1 BLOCKER и S1–S4 SHOULD-FIX** уже закрыты в этом же +PR-стеке (см. CHANGELOG `Unreleased`). Ниже — что осталось. + +### Безопасность (4 MEDIUM, фиксить до non-localhost деплоя) + +- [ ] **M-1 — `/api/events` SSE без cap'а соединений.** В + [webui.py](src/hope_hash/webui.py) per-connection + `queue.Queue(maxsize=256)` + поток на запрос. На loopback OK, но + compose биндит `0.0.0.0`. **Фикс:** `max_subscribers` cap (default 32) + + HTTP 503 при overflow + `_subscribers` cleanup на disconnect. +- [ ] **M-2 — `BitcoinRPC` cookie-file без size-guard'а.** В + [solo.py](src/hope_hash/solo.py) `Path(cookie_path).read_text()` без + лимита и `try/except`. `--rpc-cookie /tmp` уронит майнер OOM'ом. + **Фикс:** `os.stat()` → лимит 4 KiB → `OSError` → дружелюбная + CLI-ошибка. +- [ ] **M-3 — `ctypes.CDLL("libcrypto-3.dll")` DLL hijack на Windows.** + В [sha_native.py](src/hope_hash/sha_native.py) загрузка по голому + имени → DLL search order включает cwd. **Фикс:** `ctypes.WinDLL(..., + winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)` или `os.add_dll_directory()` с + известным путём. +- [ ] **M-4 — docker-compose `0.0.0.0` + Grafana `admin/admin`.** + Префикс `127.0.0.1:` на `ports`, `${GRAFANA_PASSWORD:?GRAFANA_PASSWORD + must be set}` идиома, чтобы compose отказывался стартовать без + пароля. + +### Test gaps (6 пунктов из ревью) + +- [ ] Mid-state ↔ ctypes parity sentinel (защита от регресса + endianness `_worker_ctypes` vs `_worker_hashlib_midstate`). +- [ ] `SoloClient.reader_loop` RPC failure path. +- [ ] `SoloClient` без `default_witness_commitment` (regtest / + non-segwit templates). +- [ ] `cli._build_pool_list` и `_resolve_sha_backend` — + `tests/test_cli_helpers.py`. +- [ ] `webui._serve_events` cleanup на disconnect (smoke-тест, + что `_subscribers` возвращается к baseline). +- [ ] `StatsProvider.publish_event` под concurrent + subscribe/unsubscribe. + +### Non-blocking code concerns + +- [ ] [cli.py:460-466](src/hope_hash/cli.py#L460): убрать + monkey-patch `stats_provider.update_hashrate`, поднять + `last_hashrate_ts` в `StatsSnapshot`. +- [ ] [notifier.py:307](src/hope_hash/notifier.py#L307): мёртвая + boolean clause; вероятно `if item is None or + self._stop_event.is_set():`. +- [ ] [solo.py:489-499](src/hope_hash/solo.py#L489): `deadbeef` + extranonce-маркер с 2⁻³² collision risk. Заменить на 16-байт + `os.urandom`. +- [ ] [solo.py:407-452](src/hope_hash/solo.py#L407): `submit()` синхронно + в mine-thread; задокументировать в `architecture.{en,ru}.md`. +- [ ] [webui.py:331-336](src/hope_hash/webui.py#L331): `repr(exc)` в + healthz HTTP body; на loopback мягко, но фильтр не лишний. +- [ ] [parallel.py:275-276](src/hope_hash/parallel.py#L275): bare + `except Exception: pass` на queue cleanup. + +### Docs nice-to-haves (11 пунктов) + +- [ ] **N1**: валидация `--solo` argument-tuple до BTC-address validation. +- [ ] **N2**: bilingual `argparse` help-тексты (сейчас только русские). +- [ ] **N3**: `architecture.{en,ru}.md` — упомянуть, что Telegram inbound + требует `HOPE_HASH_TELEGRAM_INBOUND=1`. +- [ ] **N6/N7**: `deploy.{en,ru}.md` §3 — `degraded` отдаёт 200, не 503. +- [ ] **N10**: `Dockerfile` `EXPOSE` — синхронизировать с compose + (8000 + 8001, не 8000 + 9090). +- [ ] **N11**: `architecture.{en,ru}.md` file-map — добавить + `_logging.py` и `__main__.py`. + +--- + ## Сознательно отложено (не включено в v0.7.0) После трёх PR'ов (ops/UX, perf/resilience, web/docs) вот что **намеренно** diff --git a/docs/handoff/final-review.md b/docs/handoff/final-review.md index 8bf6081..86535a4 100644 --- a/docs/handoff/final-review.md +++ b/docs/handoff/final-review.md @@ -6,6 +6,14 @@ **Tests:** 242 passing, ~17s wall, no flakes. **Reviewers:** code, security, docs/UX, test coverage (4 parallel agents). +> **Update (2026-05-03):** B1 BLOCKER and all four S1–S4 SHOULD-FIX +> items have been addressed in a follow-up commit on this branch. +> Tests now 244 (B1 added two regression sentinels — non-symmetric +> mainnet prevhash and a `_build_header_base` byte-position assertion). +> M-1 through M-4, the six test-gap items and the eleven docs +> nice-to-haves are tracked in [ROADMAP.md](../../ROADMAP.md) under +> *«Запланировано на v0.7.x / v0.8.0»*. + Detailed reports: - [`review-code.md`](./review-code.md) - [`review-security.md`](./review-security.md)