Per-account цепочки вместо глобальной. 2 хеша вместо 4. Bitcoin-якорение — отложено до реальных межброкерских споров. Хеши копятся сразу, Merkle строится ретроспективно.
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/сек
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 покрывает всё при батче.
Гонки при записи prev_hash устранены — два параллельных запроса к разным аккаунтам не конфликтуют. UNIQUE constraint на prev_hash внутри одного аккаунта гарантирует целостность. Merkle tree собирает всё в один корень при батче.
// 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) // → гарантирует отсутствие форков внутри аккаунта
| Было (план) | Стало (решение) | Почему |
|---|---|---|
| 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": "..." } }
Per-account цепочки дают целостность внутри аккаунта. Merkle tree собирает tx_hash всех операций за период в один merkle_root. Это доказывает что весь набор не изменён.
// 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)
Хеши копятся сразу. Merkle tree строится ретроспективно. Когда споры появятся — якорим батч в Bitcoin одной транзакцией. До тех пор: внутренняя верификация через per-account цепочки.
CRC8 даёт только 256 вариантов. Для UID FO-xxx это допустимо (human-readable), но для верификации хешей — нет.
Кто записал хеш? Без подписи любой может пересчитать цепочку.
Два параллельных запроса к одному аккаунту → fork цепочки.
JSON key order, float precision, timezone — всё влияет на хеш. Разные версии PHP могут дать разный хеш.
float(1.1) + float(2.2) ≠ 3.3 в PHP. Хеш от float — недетерминированный.
Батч собирает TX из всех тенантов. Если хранить в per-broker — нет глобального покрытия.
Кто владеет ключом для OP_RETURN? Как ротировать? Как резервировать?