Хеш-цепочки и аудит

Per-account цепочки вместо глобальной. 2 хеша вместо 4. Bitcoin-якорение — отложено до реальных межброкерских споров. Хеши копятся сразу, Merkle строится ретроспективно.

1 Почему глобальная цепочка — bottleneck
Глобальная цепочка (план)
TX #1001 → hash → prev_hash: #1000
TX #1002 → hash → prev_hash: #1001
TX #1003 → hash → prev_hash: #1002
...

Проблема: два параллельных запроса
берут один prev_hash → fork цепочки.

RoadRunner обрабатывает запросы
параллельно — это реальный сценарий.

Решение: SELECT ... FOR UPDATE
→ сериализация = bottleneck при >10 tx/сек
    
Per-account цепочки (решение)
Account #42 (Вася, USDT-TRC20):
TX A → hash → prev: genesis
TX B → hash → prev: A

Account #43 (Олег, RUB-CASH):
TX C → hash → prev: genesis
TX D → hash → prev: C

Параллелизм сохранён!
Каждый аккаунт — своя цепочка.
Merkle tree покрывает всё при батче.
    
Per-account цепочки: параллелизм сохранён, целостность тоже

Гонки при записи prev_hash устранены — два параллельных запроса к разным аккаунтам не конфликтуют. UNIQUE constraint на prev_hash внутри одного аккаунта гарантирует целостность. Merkle tree собирает всё в один корень при батче.

2 Как выглядит per-account цепочка
Account #42: Вася × USDT-TRC20
Genesis
0x0000...0000
Создание счёта
TX #1
a7b3c9...2f1e
+5,000 USDT (deposit)
TX #2
f1e2d3...8a9b
-1,000 USDT (swap)
TX #3
c4d5e6...7f0a
+200 USDT (settlement)
TX #4
???
Следующая TX
// TransactionEntity — хеширование

initial_hash = SHA256(
    account_id + instrument_id + amount +
    type + counterpart_id + created_at
)

prev_hash = последний tx_hash этого же аккаунта
           (или 0x00..00 для первой TX)

tx_hash = SHA256(initial_hash + prev_hash)

// Для верификации:
// пересчитать цепочку от genesis до текущей TX
// если хоть одна TX изменена → хеш не сходится

// UNIQUE constraint: (account_id, prev_hash)
// → гарантирует отсутствие форков внутри аккаунта
3 Упрощение: 2 хеша вместо 4
Было (план)Стало (решение)Почему
4 шага хэширования (initial, status_change, completion, final) 2 шага: initial_hash + final_hash Для арбитража достаточно 2 точек: «что было запрошено» и «чем закончилось»
Глобальная цепочка prev_hash Per-account цепочки Иначе bottleneck при параллельных запросах
Bitcoin-якорение каждого батча Отложено до межброкерских споров Хеши копятся сразу, Merkle строится ретроспективно
Промежуточные хеши обязательны Промежуточные → в hash_chain_json JSON для аудита, не для блокировки workflow
// TransactionEntity — итоговая модель

{
    id: 4821,
    account_id: 42,
    type: "swap_debit",
    amount: "-1000.00",           // Decimal, не float!
    balance_after: "4200.00",

    // Хеширование (2 точки)
    initial_hash: "a7b3c9d2...",   // что запрошено
    final_hash:   "f1e2d3c4...",   // чем закончилось

    // Цепочка (per-account)
    prev_hash:    "8b9a0f1e...",   // предыдущий tx этого аккаунта
    tx_hash:      "c4d5e6f7...",   // SHA256(final_hash + prev_hash)

    // Аудит (опционально)
    hash_chain_json: {              // промежуточные для полного аудита
        "status_change": "...",
        "completion": "..."
    }
}
4 Merkle Tree — собирает всё в один корень
10,000 операций = 2мс на построение, ~640KB

Per-account цепочки дают целостность внутри аккаунта. Merkle tree собирает tx_hash всех операций за период в один merkle_root. Это доказывает что весь набор не изменён.

Батч: 8 транзакций за час → 1 Merkle Root
Root: 9f2e...a1b3
╱           ╲
H(AB): 3c7d...f2e1
H(CD): 8a1b...d4c5
╱   ╲       ╱   ╲
H(A+B)
H(C+D)
H(E+F)
H(G+H)
│   │       │   │       │   │       │   │
TX A
TX B
TX C
TX D
TX E
TX F
TX G
TX H
// AnchorBatchEntity
{
    id: 127,
    batch_start: "2026-04-20 12:00",
    batch_end:   "2026-04-20 13:00",
    tx_count:    847,
    merkle_root: "9f2ea1b3...",        // 32 байта
    btc_txid:    null,                 // заполняется при якорении
    btc_block:   null,
    status:      "pending_anchor"      // или "anchored"
}

// Отдельная таблица anchor_batch_leaves
// для хранения листьев Merkle tree
// (позволяет доказать включение конкретной TX)
5 Bitcoin-якорение (отложено)
Решение: отложить до появления реальных межброкерских споров

Хеши копятся сразу. Merkle tree строится ретроспективно. Когда споры появятся — якорим батч в Bitcoin одной транзакцией. До тех пор: внутренняя верификация через per-account цепочки.

📦
Батч TX
847 транзакций/час
🌳
Merkle Tree
2мс, ~640KB
🔑
Merkle Root
32 байта
OP_RETURN
$0.50-2.00 за батч
⛓️
Bitcoin Block
Неизменяемо

Что доказывает Bitcoin-якорь

Доказывает:
✓ Данные существовали в момент X
✓ Данные не менялись с момента X
✓ Конкретная TX входит в батч (Merkle proof)
НЕ доказывает:
✗ Что сторона согласилась с данными
✗ Юридическую силу в суде (доп. аргумент)
✗ Что данные корректны (только целостность)
Для внутрисетевого арбитража между брокерами — работает отлично. Для суда — дополнительный аргумент, не определяющий.

OP_RETURN: экономика

OP_RETURN: 80 байт. Merkle root: 32 байта. Остаток: 48 байт на метаданные (версия, timestamp, broker_id).
Стоимость: $0.50-2.00 за транзакцию (зависит от fee congestion).
Решение: динамический батчинг — высокий fee → увеличить интервал до 4-6 часов. Fee ceiling — не больше $5 за якорение.
6 7 критичных проблем текущего плана
HIGH

CRC8 — слишком слабый checksum для UID

CRC8 даёт только 256 вариантов. Для UID FO-xxx это допустимо (human-readable), но для верификации хешей — нет.

Fix: для хешей использовать SHA256[0:4], CRC8 оставить только для UID
HIGH

Хеши без подписей не доказывают авторство

Кто записал хеш? Без подписи любой может пересчитать цепочку.

Fix: HMAC с per-broker ключом или ECDSA подпись из seed phrase (Уровень 2/3)
HIGH

Race conditions при записи prev_hash

Два параллельных запроса к одному аккаунту → fork цепочки.

Fix: UNIQUE(account_id, prev_hash) + SELECT FOR UPDATE + retry
MEDIUM

Нет канонической сериализации

JSON key order, float precision, timezone — всё влияет на хеш. Разные версии PHP могут дать разный хеш.

Fix: зафиксировать формат: sorted keys, Decimal строки, UTC timestamps
HIGH

Amount как float нельзя хешировать

float(1.1) + float(2.2) ≠ 3.3 в PHP. Хеш от float — недетерминированный.

Fix: хешировать string representation Decimal (bcmath). Всегда "1000.00", не 1000.0
MEDIUM

AnchorBatchEntity должна быть в мастер-БД

Батч собирает TX из всех тенантов. Если хранить в per-broker — нет глобального покрытия.

Fix: AnchorBatchEntity + anchor_batch_leaves — в мастер-БД (fastotc)
MEDIUM

Управление Bitcoin-ключом не описано

Кто владеет ключом для OP_RETURN? Как ротировать? Как резервировать?

Fix: платформенный ключ в HSM/KMS. Ротация раз в год. Бэкап — cold wallet.
7 Что делать прямо сейчас

Сейчас (Фаза 0-1)

Добавить tx_hash, prev_hash, initial_hash в TransactionEntity
Реализовать per-account цепочку с UNIQUE constraint
Каноническая сериализация (sorted keys, Decimal строки, UTC)
Хеши начинают копиться с первого дня

Потом (Фаза 3, когда межброкерка живая)

AnchorBatchEntity + Merkle tree
HMAC/ECDSA подписи (привязка к seed phrase)
Верификация UI (проверить цепочку в один клик)
Bitcoin OP_RETURN (когда есть реальные споры)