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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ htmlcov/
venv/
env/
*.log
*.db
*.db-journal
*.db-wal
*.db-shm
.idea/
.vscode/
.DS_Store
Expand Down
29 changes: 27 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@

## [Unreleased]

## [0.2.0] — 2026-04-30

### Добавлено
- **Multiprocessing** (`parallel.py`): N воркеров делят nonce-пространство
`[0, 2³²)` равными долями. CLI-флаг `--workers N` (default `cpu_count - 1`).
`found_queue` для шаров, `hashes_counter` для статистики.
- **EMA-хешрейт**: скользящее среднее (alpha=0.3, окно 5с) вместо мгновенного.
- **SQLite-журнал** (`storage.py`): таблицы `shares` и `sessions`, WAL-режим,
потокобезопасность. CLI-флаги `--db PATH`, `--no-db`.
- **Prometheus-экспортёр** (`metrics.py`): `/metrics` на `http.server`, метрики
`hopehash_shares_total`, `hopehash_hashrate_hps`, `hopehash_pool_difficulty`,
`hopehash_workers`, `hopehash_uptime_seconds`. CLI-флаг `--metrics-port`
(default 9090, 0 — выключить).
- **Telegram-уведомления** (`notifier.py`): через stdlib `urllib`, без
`python-telegram-bot`. Конфиг через env: `HOPE_HASH_TELEGRAM_TOKEN`,
`HOPE_HASH_TELEGRAM_CHAT_ID`. События: started / stopped / share_accepted /
block_found.
- 41 новый юнит-тест (storage: 9, metrics: 16, notifier: 16). Всего **56 тестов**.

### Изменено
- `mine()` принимает опциональные `store`, `metrics`, `notifier` (None — disabled).
- `__init__.py` re-export `ShareStore`, `Metrics`, `MetricsServer`, `TelegramNotifier`.
- `.gitignore` дополнен `*.db`, `*.db-journal`, `*.db-wal`, `*.db-shm`.

## [0.1.0] — 2026-04-30

### Добавлено
Expand All @@ -19,5 +43,6 @@
- 15 юнит-тестов на криптографические функции (`unittest`).
- Реструктуризация в `src/`-layout с пакетом `hope_hash`.

[Unreleased]: https://github.com/KruglikovskiiPA/Hope-Hash/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/KruglikovskiiPA/Hope-Hash/releases/tag/v0.1.0
[Unreleased]: https://github.com/devAsmodeus/Hope-Hash/compare/v0.2.0...HEAD
[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
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,21 @@
- [x] CI matrix Python 3.11/3.12/3.13 × ubuntu/windows/macos
- [ ] Конфиг через CLI/YAML — перенесено в Уровень 1

**Уровень 1 — производительность и наблюдаемость: завершён.**

- [x] Multiprocessing: N воркеров (default `cpu_count - 1`), флаг `--workers`
- [x] EMA-хешрейт (alpha=0.3, окно 5с)
- [x] SQLite-журнал шаров и сессий (`storage.py`, флаг `--db`)
- [x] Prometheus-метрики на `/metrics` (`metrics.py`, флаг `--metrics-port`)
- [x] Telegram-уведомления (через stdlib urllib, env-конфиг)
- [ ] TUI на `rich` / `curses` — отложено (зависимости либо ограниченная Win-поддержка)
- [ ] Команды Telegram-бота (`/stats`, `/restart`) — отложено

**Не сделано / известные ограничения:**

- Один поток → ~50–200 KH/s на ноуте. Нет multiprocessing.
- Нет персистентной статистики (логов, БД).
- Нет UI — только консоль через `logging`.
- Нет UI — только консоль через `logging` и `/metrics` через HTTP.
- Только Stratum V1, без `mining.suggest_difficulty` и без Stratum V2.
- C/Rust/SIMD/GPU — Уровни 2–3, ещё впереди.

---

Expand All @@ -60,15 +69,22 @@
├── src/hope_hash/
│ ├── __init__.py ← публичный API + __version__
│ ├── __main__.py ← `python -m hope_hash`
│ ├── cli.py ← argparse, точка входа
│ ├── miner.py ← mine(), supervisor_loop, run_session
│ ├── cli.py ← argparse, точка входа, инициализация observers
│ ├── miner.py ← mine() оркестратор + supervisor_loop
│ ├── parallel.py ← multiprocessing воркеры nonce-loop
│ ├── stratum.py ← StratumClient (TCP + JSON-RPC)
│ ├── block.py ← double_sha256, swap_words, target, merkle
│ ├── storage.py ← SQLite журнал шаров и сессий
│ ├── metrics.py ← Prometheus экспортёр (http.server)
│ ├── notifier.py ← Telegram через urllib
│ ├── _logging.py ← настройка logger("hope_hash")
│ └── py.typed ← PEP 561 marker
└── tests/
├── conftest.py ← общие фикстуры (заготовка)
└── test_block.py ← 15 unittest-тестов на чистые функции
├── test_block.py ← 15 тестов на чистые функции
├── test_storage.py ← 9 тестов на SQLite журнал
├── test_metrics.py ← 16 тестов на Prometheus экспортёр
└── test_notifier.py ← 16 тестов на Telegram (через mock)
```

---
Expand All @@ -92,10 +108,27 @@ python -m hope_hash <BTC_адрес> [имя_воркера]
hope-hash bc1q5n2x4pvxhq8sxc7ck3uxq8sxc7ck3uxqzfm2py mylaptop
```

**Расширенные опции:**

```bash
hope-hash <BTC_адрес> mylaptop \
--workers 8 \ # число процессов (default: cpu_count - 1)
--db ./shares.db \ # путь к SQLite (default: hope_hash.db)
--metrics-port 9090 # Prometheus /metrics (0 — отключить)
```

**Telegram-уведомления (опционально):** задать env vars и просто запустить:

```bash
export HOPE_HASH_TELEGRAM_TOKEN=123456:abcdef-your-bot-token
export HOPE_HASH_TELEGRAM_CHAT_ID=123456789
hope-hash <BTC_адрес>
```

**Тесты:**

```bash
python -m unittest discover -s tests -v
python -m unittest discover -s tests -v # 56 тестов
```

BTC-адрес нужен валидный (любой формат: `1...`, `3...`, `bc1q...`, `bc1p...`). Можно завести в любом некастодиальном кошельке — например, **Sparrow**, **Electrum**, **Wasabi**. Имя воркера — произвольная строка.
Expand Down
17 changes: 10 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@

## Уровень 1 — лёгкие апгрейды (вечер каждый)

**Статус (2026-04-30):** производительность + наблюдаемость — закрыто.
TUI и команды Telegram — отложены.

Видимые фичи, не требующие глубокой переработки.

### Производительность

- [ ] **Multiprocessing.** Каждый CPU-ядро — отдельный процесс, общий `extranonce2` через `multiprocessing.Value`. Х2-х8 к хешрейту в зависимости от железа.
- [ ] **Замер реального хешрейта по окнам.** Сейчас считается «за интервал печати», правильнее — скользящее окно с EMA (exponential moving average).
- [x] **Multiprocessing.** `parallel.py`, CLI `--workers N`. nonce-пространство `[0, 2³²)` делится поровну между N процессами. found_queue + hashes_counter (`mp.Value('Q')`). На 16-CPU машине default = 15 воркеров.
- [x] **EMA-хешрейт.** alpha=0.3, окно 5с. Сэмпл = дельта счётчика / dt. Логируется и в `[stats]`, и в Prometheus gauge `hopehash_hashrate_hps`.

### UI/UX

Expand All @@ -41,14 +44,14 @@

### Telegram-бот

- [ ] **Уведомления о ключевых событиях:** старт майнера, потеря соединения, принятый шар, (мечты) найденный блок. Через `python-telegram-bot` или просто curl на `api.telegram.org`.
- [ ] **Команды `/stats`, `/restart`, `/stop`** через того же бота.
- [x] **Исходящие уведомления.** `notifier.py` через stdlib `urllib`, без `python-telegram-bot`. События: started / stopped / share_accepted / block_found / disconnected / reconnected. Конфиг через env (`HOPE_HASH_TELEGRAM_TOKEN`, `HOPE_HASH_TELEGRAM_CHAT_ID`). Если переменные не заданы — модуль молча disabled.
- [ ] **Входящие команды `/stats`, `/restart`, `/stop`** через тот же бот (long polling). Отложено.

### Логи и метрики

- [ ] **SQLite-журнал шар.** Каждый принятый шар → строка с timestamp, job_id, hash, difficulty.
- [ ] **Экспорт в Prometheus формат** через эндпоинт `/metrics` на `http.server`.
- [ ] **Grafana-дашборд** с готовым JSON для импорта.
- [x] **SQLite-журнал** (`storage.py`). Таблицы `shares` (ts, job_id, nonce_hex, hash_hex, difficulty, accepted, is_block) и `sessions`. WAL-режим, потокобезопасность через `threading.Lock`.
- [x] **Prometheus-экспортёр** (`metrics.py`). Эндпоинт `/metrics` на `http.server` (`ThreadingHTTPServer` в фоновой нити). Метрики: `hopehash_shares_total`, `hopehash_hashrate_hps`, `hopehash_pool_difficulty`, `hopehash_workers`, `hopehash_uptime_seconds`. CLI `--metrics-port` (0 — выключить).
- [ ] **Grafana-дашборд** с готовым JSON для импорта. Отложено.

---

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ dependencies = []
dynamic = ["version"]

[project.urls]
Repository = "https://github.com/KruglikovskiiPA/Hope-Hash"
Issues = "https://github.com/KruglikovskiiPA/Hope-Hash/issues"
Repository = "https://github.com/devAsmodeus/Hope-Hash"
Issues = "https://github.com/devAsmodeus/Hope-Hash/issues"

[project.scripts]
hope-hash = "hope_hash.cli:main"
Expand Down
9 changes: 8 additions & 1 deletion src/hope_hash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
"""Hope-Hash — учебный solo BTC miner на чистом stdlib."""

from .block import build_merkle_root, difficulty_to_target, double_sha256, swap_words
from .metrics import Metrics, MetricsServer
from .miner import mine
from .notifier import TelegramNotifier
from .storage import ShareStore
from .stratum import StratumClient

__version__ = "0.1.0"
__version__ = "0.2.0"
__all__ = [
"double_sha256",
"swap_words",
"difficulty_to_target",
"build_merkle_root",
"StratumClient",
"mine",
"ShareStore",
"Metrics",
"MetricsServer",
"TelegramNotifier",
"__version__",
]
87 changes: 78 additions & 9 deletions src/hope_hash/cli.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,93 @@
"""Точка входа CLI: argparse, запуск supervisor + mine()."""
"""Точка входа CLI: argparse, запуск supervisor + mine() + observers."""

import argparse
import multiprocessing
import os
import threading
import time
from pathlib import Path

from ._logging import logger, setup_logging
from .metrics import Metrics, MetricsServer
from .miner import mine, supervisor_loop
from .notifier import TelegramNotifier
from .storage import ShareStore
from .stratum import StratumClient


POOL_HOST = "solo.ckpool.org"
POOL_PORT = 3333


def main():
setup_logging()
def _default_workers() -> int:
"""Один CPU оставляем сетевой части/IO. Минимум — 1 воркер."""
cpu = os.cpu_count() or 1
return max(1, cpu - 1)


def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="hope_hash",
description="Учебный solo BTC miner на чистом stdlib.",
)
parser.add_argument("btc_address", help="BTC-адрес для выплат (на него уйдёт награда).")
parser.add_argument("worker_name", nargs="?", default="py01",
help="Имя воркера (по умолчанию: py01).")
args = parser.parse_args()
parser.add_argument(
"--workers", type=int, default=_default_workers(),
help=f"Число процессов-воркеров (по умолчанию: {_default_workers()} = cpu_count - 1).",
)
parser.add_argument(
"--db", type=str, default="hope_hash.db",
help="Путь к SQLite-журналу шаров (по умолчанию: hope_hash.db).",
)
parser.add_argument(
"--no-db", action="store_true",
help="Отключить SQLite-журнал (--db игнорируется).",
)
parser.add_argument(
"--metrics-port", type=int, default=9090,
help="Порт Prometheus /metrics (по умолчанию: 9090, 0 — отключить).",
)
return parser.parse_args()


btc_address = args.btc_address
worker_name = args.worker_name
def main():
# Защитный вызов: на Windows multiprocessing требует freeze_support()
# при запуске через `python -m hope_hash`. Без него spawn-дети могут
# пытаться повторно стартовать main() и упасть.
multiprocessing.freeze_support()

setup_logging()
args = _parse_args()
n_workers = max(1, args.workers)

# ─── observers ───
# Все три опциональны и не зависят друг от друга. Каждый сам решает,
# включаться ли (notifier — по env vars; metrics — по порту; store — по флагу).
store: ShareStore | None = None
if not args.no_db:
store = ShareStore(Path(args.db))

metrics: Metrics | None = None
metrics_server: MetricsServer | None = None
if args.metrics_port > 0:
metrics = Metrics()
metrics_server = MetricsServer(metrics, port=args.metrics_port)
metrics_server.start()

notifier = TelegramNotifier.from_env()
notifier.notify_started(args.btc_address, args.worker_name)

if store is not None:
session_id = store.start_session(POOL_HOST, args.btc_address, args.worker_name)
else:
session_id = None

# ─── сетевая часть и mine() ───
stop = threading.Event()
client = StratumClient(POOL_HOST, POOL_PORT, btc_address, worker_name, stop_event=stop)
client = StratumClient(POOL_HOST, POOL_PORT, args.btc_address, args.worker_name,
stop_event=stop)

# Сетевая часть живёт в отдельной нити-супервизоре: она держит коннект,
# переподключается при разрывах и сама поднимает reader_loop. main thread
Expand All @@ -38,13 +96,14 @@ def main():
name="stratum-supervisor", daemon=False)
supervisor.start()

logger.info("[main] жду первый job от пула...")
logger.info(f"[main] жду первый job от пула... (воркеров: {n_workers})")
while client.current_job is None and not stop.is_set():
time.sleep(0.1)

try:
if not stop.is_set():
mine(client, stop)
mine(client, stop, n_workers=n_workers,
store=store, metrics=metrics, notifier=notifier)
except KeyboardInterrupt:
logger.info("[main] остановка по Ctrl+C")
finally:
Expand All @@ -55,3 +114,13 @@ def main():
supervisor.join(timeout=5)
if supervisor.is_alive():
logger.warning("[main] supervisor не остановился за 5с")

# Закрываем observers последними, чтобы дать им зафиксировать финальные события.
notifier.notify_stopped()
notifier.shutdown()
if metrics_server is not None:
metrics_server.stop()
if store is not None:
if session_id is not None:
store.end_session(session_id)
store.close()
Loading
Loading