golang

От стартапа к протоколу: Почему мы решили написать свой «PostgreSQL для финансов»

  • суббота, 24 января 2026 г. в 00:00:14
https://habr.com/ru/articles/988320/

В мире разработки есть негласное правило: не пишите свою криптографию. В финтехе должно быть похожее правило: не пишите свой леджер (ledger) на SQL, если планируете масштабироваться.

Меня зовут [Имя], и я хочу рассказать, как мы прошли классический путь «изобретения велосипеда», набили шишки на race condition-ах и в итоге поняли, что индустрии нужен не очередной необанк, а открытый стандарт финансового учета. Так появился проект Qazna.

В этой статье я не буду продавать вам продукт. Я покажу код, архитектурные решения и то, как мы пытаемся сделать «Linux для финансов».

Проблема: Ловушка UPDATE accounts

Каждый финтех-стартап начинается одинаково. Вы поднимаете PostgreSQL, создаете таблицу users и добавляете колонку balance.

-- Начало пути

CREATE TABLE users (

id UUID PRIMARY KEY,

balance DECIMAL(18, 2) NOT NULL DEFAULT 0

);

Перевод денег выглядит как транзакция:

BEGIN;

UPDATE users SET balance = balance - 100 WHERE id = 'ALICE';

UPDATE users SET balance = balance + 100 WHERE id = 'BOB';

COMMIT;

Это работает, пока у вас 100 пользователей. Но когда нагрузка растет, начинаются проблемы:

  1. Блокировки (Locking): Частые обновления одной строки (например, счет мерчанта) приводят к lock contention.

  2. Гонки (Race Conditions): Без правильных уровней изоляции транзакций можно уйти в минус паралельными запросами.

  3. Потеря истории: Когда вы делаете UPDATE, вы перезаписываете прошлое. Если баланс Алисы стал -50, вы не знаете почему. Это был баг? Взлом? Ошибка оператора?

Мы поняли, что строим учетную систему на базе БД общего назначения. Это как писать B-Tree на каждом проекте, вместо того чтобы просто использовать готовую базу данных.

Решение: Детерминированный стейт-машина

Вдохновившись LMAX Disruptor и принципами блокчейна (но без майнинга и консенсуса, так как мы работаем в доверенной среде), мы пришли к архитектуре Append-Only Log.

В Qazna баланс — это не число в ячейке, а агрегатная функция от истории всех транзакций.

Архитектура

Qazna состоит из двух частей:

  1. Core (Rust): "Ядро". Однопоточная (для детерминизма) стейт-машина, которая принимает батчи операций и применяет их последовательно.

  2. API (Go): "Оболочка". Принимает HTTP/gRPC запросы, делает аутентификацию и складывает команды в очередь ядра.

Strict Determinism

Главное правило: Если мы проиграем лог событий заново, мы должны получить идентичное состояние байт-в-байт.

Вот как выглядит структура транзакции в Rust (core/ledger/src/lib.rs):

#[derive(Clone, Debug, PartialEq, Eq)]

pub struct Transaction {

pub id: String,

pub created_at: DateTime<Utc>,

pub movements: Vec<Movement>,

pub sequence: u64,

pub hash: String,

pub prev_hash: Option<String>, // Hash chaining

}

Обратите внимание на prev_hash. Каждая транзакция криптографически связана с предыдущей. Это создает неизменяемую цепочку (по сути, приватный блокчейн), что гарантирует аудируемость. Если кто-то "подкрутит" баланс в базе данных напрямую, хэши перестанут сходиться, и система откажется запускаться при следующей проверке целостности.

Атомарность мульти-ассетных переводов (Atomic Swaps)

Вторая большая проблема финтеха — Counterparty Risk. Алиса хочет обменять свои USD на EUR Боба.

  1. Алиса отправляет USD.

  2. Боб... исчезает.

В традиционных базах вам нужно городить сложные саги (Sagas) или двухфазные коммиты (2PC). В Qazna мы сделали это на уровне примитива ядра.

Метод execute принимает список движений (movements) и выполняет их все или ничего:

// core/ledger/src/lib.rs

pub fn execute(

&self,

movements: Vec<Movement>,

idempotency_key: Option<&str>,

) -> Result<Transaction, LedgerError> {

// 1. Предварительная проверка (хватает ли денег у всех участников?)

let mut changes: HashMap<(String, String), i64> = HashMap::new();

// Агрегируем все изменения по счетам

for m in &movements {

*changes.entry((m.from_id.clone(), m.currency.clone())).or_default() -= m.amount;

*changes.entry((m.to_id.clone(), m.currency.clone())).or_default() += m.amount;

}

// Проверяем, не уходит ли кто-то в минус

for ((acc_id, currency), change) in &changes {

if *change < 0 {

let acc = state.accounts.get(acc_id).ok_or(LedgerError::NotFound)?;

let current_bal = acc.balances.get(currency).unwrap_or(&0);

if *current_bal + change < 0 {

return Err(LedgerError::InsufficientFunds);

}

}

}

// 2. Если все ок — применяем изменения и записываем в лог

// ...

}

Это позволяет проводить безопасные обмены валют (PvP - Payment vs Payment) или покупку активов (DvP - Delivery vs Payment) одним запросом.

API: Protobuf как контракт

Мы используем gRPC и Protobuf для строгого контракта. Никаких JSON без схемы.

// api/proto/qazna/v1/ledger.proto

message ExecuteRequest {

repeated Movement movements = 1;

string idempotency_key = 2;

}

message Movement {

string from_id = 1;

string to_id = 2;

string currency = 3;

int64 amount = 4;

}

Идемпотентность (idempotency_key) встроена в ядро. Если сеть моргнула и вы послали запрос повторно с тем же ключом, Qazna вернет результат уже выполненной транзакции, не выполняя её дважды.

Зачем мы открыли код?

Мы могли бы продавать Qazna как SaaS. Но мы верим, что фундаментальная инфраструктура должна быть открытой. Linux стал стандартом не потому что он был самым красивым продуктом, а потому что он был открытой базой, на которой все могли строить.

Финансовый мир сейчас — это тысячи закрытых "велосипедов". Мы хотим дать инженерам надежный, быстрый и открытый движок, чтобы они могли фокусироваться на продукте, а не на том, как правильно лочить строки в базе данных.

Что дальше?

Проект написан на Rust (Core) и Go (API). Мы активно ищем контрибьюторов, которым интересны:

  • High-load системы и оптимизация Rust.

  • Распределенные системы (планируем Raft-кластеризацию).

  • Формальная верификация (TLA+).

Код доступен на GitHub

Страница с документацией

Буду рад ответить на технические вопросы в комментариях!