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)