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
35 changes: 35 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.7.0"
}
56 changes: 55 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### Добавлено
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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`.
Expand Down
74 changes: 74 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) вот что **намеренно**
Expand Down
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion docs/deploy.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <BTC_ADDRESS> <WORKER_NAME> — the literal "mybox"
# is just the worker_name shown to the pool; pick anything you like.
```

## 7. Behind a reverse proxy
Expand Down
5 changes: 4 additions & 1 deletion docs/deploy.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
# позиционные аргументы: <BTC_ADDRESS> <WORKER_NAME> — литерал "mybox"
# здесь это просто имя воркера для пула; подставьте любое.
```

## 7. За reverse-proxy
Expand Down
8 changes: 8 additions & 0 deletions docs/handoff/final-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,7 +26,6 @@ classifiers = [
"Topic :: Security :: Cryptography",
]
dependencies = []
dynamic = ["version"]

[project.urls]
Repository = "https://github.com/devAsmodeus/Hope-Hash"
Expand All @@ -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"]
Expand Down
19 changes: 19 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
2 changes: 1 addition & 1 deletion src/hope_hash/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 16 additions & 12 deletions src/hope_hash/solo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-строку,
Expand Down
Loading
Loading