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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@

## [Unreleased]

## [0.3.0] — 2026-04-30

### Добавлено
- **Mid-state SHA-256** (`parallel.py`): block header 80 байт = 64 + 16.
Первые 64 байта — константа в пределах одного nonce-цикла. Pre-compute
через `hashlib.sha256().copy()` даёт ≈×1.5–2 к хешрейту без зависимостей.
- **Demo-режим** (`demo.py`): `hope-hash --demo [--workers N] [--demo-diff DIFF]`.
Запускается без подключения к пулу; ищет nonce для синтетического заголовка
с низкой сложностью. Полезен для презентаций и offline-тестирования.
- **Vardiff** (`stratum.py`): метод `suggest_difficulty(diff)` и
CLI-флаг `--suggest-diff FLOAT`. Отправляет `mining.suggest_difficulty`
после авторизации, чтобы запросить у пула удобную сложность для CPU.
- 3 новых теста `TestMidstateSha256` в `test_block.py`. Всего **59 тестов**.

### Исправлено
- `miner.py`: голый `except Exception: pass` на `found_queue.get_nowait()`
заменён на `except queue.Empty:` — реальные ошибки больше не маскируются.
- `parallel.py`: аналогичный fix в `stop_pool` при drain-е очереди.
- `miner.py`, `parallel.py`: `time.time()` → `time.perf_counter()` для
всех относительных интервалов (EMA, alive-check, drain-deadline).
Защищает от ложных скачков при корректировке системных часов (NTP).

## [0.2.0] — 2026-04-30

### Добавлено
Expand Down
69 changes: 60 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@
- `src/hope_hash/block.py` — чистые функции: `double_sha256`, `swap_words`,
`difficulty_to_target`, `build_merkle_root`. Без сайд-эффектов.
- `src/hope_hash/stratum.py` — класс `StratumClient` (TCP + JSON-RPC).
- `src/hope_hash/parallel.py` — `worker()`, `start_pool()`, `stop_pool()`.
Mid-state SHA-256 через `hashlib.sha256().copy()`.
- `src/hope_hash/miner.py` — `mine()`, `run_session()`, `supervisor_loop()`.
- `src/hope_hash/demo.py` — `run_demo()` — offline-майнинг с синтетическим заголовком.
- `src/hope_hash/storage.py` — SQLite-журнал шаров (`ShareStore`).
- `src/hope_hash/metrics.py` — Prometheus-экспортёр (`Metrics`, `MetricsServer`).
- `src/hope_hash/notifier.py` — Telegram-уведомления через stdlib `urllib`.
- `src/hope_hash/cli.py` — argparse-точка входа `main()`, константы пула.
- `src/hope_hash/_logging.py` — приватная настройка логгера `hope_hash`.
- `src/hope_hash/__init__.py` — публичный API + `__version__`.
- `src/hope_hash/__main__.py` — для `python -m hope_hash`.
- `tests/test_block.py` — 15 тестов на чистые функции.
- `tests/test_block.py` — тесты на чистые функции + mid-state.

## Архитектура (не менять без обсуждения)

Expand Down Expand Up @@ -94,20 +100,65 @@ python -m hope_hash <BTC_адрес> [имя_воркера]
python -m unittest discover -s tests -v
```

## Self-learning loop (на будущее)
## Self-learning loop

Когда накопится опыт работы агента над проектом — создать `learnings.md`
с разделами **What Has Worked / What Has Failed / Patterns and Preferences
/ Open Questions**. Формат записи:
После каждой сессии с реальными уроками — обновляй `learnings.md`.
При обнаружении нового непреложного инварианта — добавляй в этот файл.

Формат записи в `learnings.md`:
```
**[YYYY-MM-DD] — [тип задачи]**
- Observation: что замечено
- Action: что делать / чего избегать дальше
- Observation: конкретное наблюдение (не общая фраза)
- Action: что делать / чего избегать — применимо в будущих сессиях
- Confidence: high / medium / low
```

Правила: архивировать при превышении 80–100 строк, удалять устаревшее,
не добавлять записи без конкретики (vague entries едят контекст).
не добавлять записи без конкретики.

Сейчас файла нет — создать при первом реальном уроке.
---

## META: Как писать правила (инструкции для агента)

Этот раздел — самый важный. Он объясняет, **как** добавлять новые правила
в CLAUDE.md и learnings.md, чтобы документ не деградировал.

### Принципы хорошего правила

1. **Причина первична.** Формула: «[Причина] — поэтому [директива]».
> «SHA-256 endianness проверен против mainnet-блоков — NEVER переписывать.»
Без причины правило выглядит как суеверие и будет проигнорировано.

2. **Абсолютные директивы для критического.** NEVER / ALWAYS / MUST / MUST NOT —
только для правил, нарушение которых ломает корректность или безопасность.
Для preference используй «предпочитать» / «по умолчанию».

3. **Конкретность > обобщение.**
> ❌ «не злоупотреблять try/except»
> ✅ «не ловить bare `except Exception:` на Queue.get_nowait — только `queue.Empty`»

4. **Один паттерн = одна запись.** Если новое правило похоже на существующее —
обнови существующее, не создавай дубль.

5. **Не раздувай.** Правило, которое можно вывести из здравого смысла — не нужно.
Пиши только то, что неочевидно или было нарушено на практике.

### Когда обновлять CLAUDE.md (этот файл)

- Новый архитектурный инвариант, нарушение которого сломает проект.
- Новое соглашение (Conventions), применимое ко всему коду.
- Антипаттерн (Patterns to avoid), реально встреченный в работе агента.

### Когда писать в learnings.md

- Конкретный урок из реального запуска, теста или ошибки агента.
- Что сработало / не сработало в конкретной задаче.
- Вопрос, требующий будущего расследования (Open Questions).

### Как обновлять при ошибке

Когда агент допустил ошибку и пользователь просит зафиксировать урок:
1. Абстрагируй: найди общий паттерн, а не конкретную деталь задачи.
2. Запиши в learnings.md под **What Has Failed**.
3. Если паттерн системный — добавь в **Patterns to avoid** в CLAUDE.md.
4. Укажи `Confidence: low` если урок из одного случая; `high` если повторялся.
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ TUI и команды Telegram — отложены.

- [ ] **C-extension для SHA-256.** Через `cffi` или `ctypes` дёргать `EVP_DigestUpdate` из OpenSSL — даст 5–10× к хешрейту над pure-Python `hashlib`.
- [ ] **SIMD-реализация SHA-256.** AVX2 (8 хешей параллельно) или AVX-512 (16). Можно взять готовое из репо `intel-ipsec-mb` или `sha-2-multihash`. Пишется как C-extension, дёргается из Python.
- [ ] **Mid-state кэширование.** Block header — это 80 байт, последние 12 (часть merkle, ntime, nbits, nonce) меняются. Первые 64 — статичны для одной работы, их SHA-256 mid-state можно посчитать один раз и переиспользовать. Х2 к хешрейту.
- [x] **Mid-state кэширование.** `hashlib.sha256().copy()` после первых 64 байт — константа в рамках nonce-цикла. Реализовано в `parallel.worker` (v0.3.0). Прирост ≈×1.5–2, zero deps.

### Архитектура

- [ ] **Множественные пулы с failover.** Список `pool1, pool2, pool3` в конфиге, при потере pool1 — переключение на pool2 без остановки воркеров.
- [ ] **Несколько воркеров на разных пулах одновременно.** Распределённая работа с разными адресами/именами.
- [ ] **Поддержка vardiff.** Сейчас принимаем сложность как есть — научиться запрашивать через `mining.suggest_difficulty` для CPU-friendly значения (низкая сложность = чаще шары = быстрее обратная связь).
- [x] **Поддержка vardiff.** `mining.suggest_difficulty` после авторизации. CLI-флаг `--suggest-diff FLOAT`. Реализовано в `stratum.py` (v0.3.0).

### Web-морда

Expand Down Expand Up @@ -105,7 +105,7 @@ TUI и команды Telegram — отложены.

Не путь развития, а отдельные мини-проекты, которые можно реализовать поверх кода.

- [ ] **Demo-режим без подключения к пулу.** Симулирует работу с искусственно низкой сложностью (`diff=0.0001`), шары валятся часто, можно визуально пройти весь цикл. Идеально для презентаций и обучения.
- [x] **Demo-режим без подключения к пулу.** `hope-hash --demo [--demo-diff DIFF]`. Синтетический заголовок, low-diff target, multiprocessing-воркеры. Реализовано в `demo.py` (v0.3.0).
- [ ] **«Гуманизированная» статистика.** «При твоём хешрейте средний шанс найти блок: раз в 47 миллиардов лет». Считается из текущего сетевого difficulty (получаем через bitcoin-core RPC или публичные API типа `mempool.space`).
- [ ] **Lottery-визуализация.** Каждый хеш — точка на canvas. Цвет = первые 3 байта хеша. Просто красивая бесконечная анимация. Можно собрать на `pygame` или в браузере через WebSocket.
- [ ] **Бенчмарк-режим.** Прогоняет одну и ту же работу через все доступные backend-ы (pure Python, ctypes, C-extension, Rust, OpenCL) и печатает таблицу хешрейтов. Хороший способ показать, насколько Python медленный.
Expand Down
96 changes: 96 additions & 0 deletions learnings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# learnings.md — Hope-Hash self-learning log

Живая память агента между сессиями. Формат:

```
**[YYYY-MM-DD] — [тип задачи]**
- Observation: конкретное наблюдение
- Action: что делать / чего избегать
- Confidence: high / medium / low
```

Архивировать при превышении 80–100 строк.

---

## What Has Worked

**[2026-04-30] — Performance: mid-state SHA-256**
- Observation: `hashlib.sha256().copy()` после первых 64 байт block header
(версия + prevhash + merkle_root[:28]) корректно воспроизводит full double_sha256.
Тесты `TestMidstateSha256` подтвердили идентичность на 3 векторах.
- Action: для любого SHA-256 hot-path проверять, есть ли константный 64-байтный
префикс, который можно pre-compute. Два `update()` вместо конкатенации — без разницы
по производительности, но читабельнее.
- Confidence: high

**[2026-04-30] — Bug fix: queue exception handling**
- Observation: `except Exception: pass` на `Queue.get_nowait()` маскировал реальные
ошибки (unpickling, закрытый queue). Правильный паттерн — `except queue.Empty:`.
- Action: ALWAYS использовать `queue.Empty` при `get_nowait()` / `get(timeout=...)`.
NEVER bare `except Exception:` в tight loops.
- Confidence: high

**[2026-04-30] — Timing: perf_counter vs time**
- Observation: `time.time()` может «прыгать» при NTP-синхронизации. EMA-сэмплы
при этом давали отрицательное или аномально большое delta-t.
- Action: `time.perf_counter()` для всех относительных интервалов (EMA, alive-check,
drain deadline). `time.time()` только для абсолютных меток (SQLite timestamp,
Telegram event time).
- Confidence: high

**[2026-04-30] — Architecture: observer callback pattern**
- Observation: `on_share_result: Optional[Callable[[int, bool], None]]` на `StratumClient`
позволяет `mine()` подписаться на pool responses без coupling между stratum и storage.
`_submit_req_ids: set` + отдельный `_submit_lock` изолируют submit-ответы от
других ответов пула (suggest_difficulty, authorize).
- Action: для любой новой «ожидаемой» Stratum-операции — добавлять отдельный
`req_id` tracking, а не ловить все `result` ответы в `_handle_message`.
- Confidence: high

---

## What Has Failed

**[2026-04-30] — Audit depth: первичный аудит пропустил семантический баг**
- Observation: `store.record_share(accepted=True)` в `miner.py` записывает шар как
«принят», хотя это означает лишь «отправлен клиентом». Пул может отклонить.
Обнаружено только при втором глубоком аудите.
- Action: при добавлении observer-хуков проверять семантику булевых флагов.
`submitted` ≠ `accepted`. Подумать про отдельный флаг или callback на pool response.
- Confidence: medium

---

**[2026-04-30] — Protocol: authorize response check**
- Observation: `subscribe_and_authorize` не читала ответ на `mining.authorize`.
Пул мог отклонить авторизацию (wrong address format, banned worker), майнер
продолжал работу в «немом» режиме, все submits молча отклонялись.
- Action: после любого Stratum-запроса с ответом — loop `while True` до нужного
`id`, побочные сообщения через `_handle_message`. Pattern уже был для subscribe.
- Confidence: high

## Patterns and Preferences

- IPC-объекты (Queue, Value, Event) ВСЕГДА создаются в main process, не в воркерах.
Windows spawn-mode требует pickle-able аргументов — hashlib объект не pickle-able.
- multiprocessing.Queue ВСЕГДА нужно drain перед join() на Windows (deadlock).
- Observers (store/metrics/notifier) подключаются опциональными хуками; `None = disabled`.
Не смешивать бизнес-логику с observer-логикой.
- hot-path в `parallel.worker`: никаких Python-level оптимизаций без бенчмарка до/после.
Baseline замеряется через `[stats]` строки, не через синтетический timeit.

---

## Open Questions

- [x] `store.record_share(accepted=True)` — ИСПРАВЛЕНО в v0.3.0. Теперь `accepted=False`
при записи, `on_share_result` callback обновляет через `update_share_accepted()`.
- [x] `client.difficulty` race condition — ИСПРАВЛЕНО. `job_lock` покрывает difficulty
writer (stratum) и reader (mine()); локальная `current_diff` на весь job-цикл.
- [ ] `mining.suggest_difficulty` — CKPool реально снижает difficulty в ответ?
Проверить в production-запуске с `--suggest-diff 0.001`.
- [ ] `hashlib.copy()` overhead: насколько дешевле на CPython 3.11 vs 3.12 vs 3.13?
CI проходит на всех версиях, но benchmark не делали.
- [x] Submit response tracking — ИСПРАВЛЕНО. `_submit_req_ids` + `_submit_lock` в
`StratumClient` изолируют mining.submit ответы от остальных.
4 changes: 3 additions & 1 deletion src/hope_hash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""Hope-Hash — учебный solo BTC miner на чистом stdlib."""

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

__version__ = "0.2.0"
__version__ = "0.3.0"
__all__ = [
"double_sha256",
"swap_words",
"difficulty_to_target",
"build_merkle_root",
"StratumClient",
"mine",
"run_demo",
"ShareStore",
"Metrics",
"MetricsServer",
Expand Down
4 changes: 4 additions & 0 deletions src/hope_hash/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def swap_words(hex_str: str) -> bytes:
в заголовок блока его надо положить именно так. Главный gotcha Stratum V1.
"""
raw = bytes.fromhex(hex_str)
if len(raw) % 4 != 0:
raise ValueError(f"swap_words требует кратность 4 байтам, получено {len(raw)}")
return b"".join(raw[i:i+4][::-1] for i in range(0, len(raw), 4))


Expand All @@ -28,6 +30,8 @@ def difficulty_to_target(diff: float) -> int:
Pool-сложность → численный target. Шар принимается, если SHA256d(header) <= target.
diff=1 соответствует базовому target Bitcoin diff-1.
"""
if diff <= 0:
raise ValueError(f"difficulty должна быть положительной, получено {diff}")
return int(DIFF1_TARGET / diff)


Expand Down
Loading
Loading