Перейти к содержанию

06. Детерминированные ID (UUID v5)

Все TigerBeetle account ID и transfer ID в Payment Manager выводятся детерминированно из имени аккаунта / идентификатора intent через UUID v5 c фиксированными namespace'ами. Это даёт верифицируемость без чтения БД, идемпотентность повторных записей и возможность сверки с внешними системами.

Назначение

TigerBeetle оперирует 128-битными числовыми ID для аккаунтов и трансферов. Чтобы не хранить отдельный «реестр» и не плодить state, PM формирует эти ID детерминированно:

  1. На основе человекочитаемого ключа (имя аккаунта, либо intentId:index для трансфера).
  2. Через uuidv5(input, namespace) получаем стабильный 128-битный UUID.
  3. Преобразуем UUID в bigint (hex без дефисов) → это и есть TB ID.

Формально:

tbId = BigInt('0x' + uuidv5(input, NAMESPACE).replace(/-/g, ''))

Никакой случайности, никаких счётчиков — только чистая функция от входа.

Namespaces

Все namespace UUID объявлены как фиксированные константы в src/ledger/id-gen.ts и src/ledger/accounts.ts. Менять их нельзя (см. раздел ниже).

Namespace константа UUID v5 namespace Где определён Что генерирует Формула (input для uuidv5)
TB_NS 3e7b4a1c-9f2d-5e8a-b6c3-4d1f07e2a9b5 src/ledger/accounts.ts TB account ID (128-bit) accountName(type, opts) — см. ниже
TRANSFER_NS 7a9f3c1e-5b2d-4e8a-9f6c-3d1e07a2b5c4 src/ledger/id-gen.ts TB transfer ID (128-bit) ${intentId}:${index}

Имена аккаунтов (input для TB_NS)

accountName(type, opts) строит строку по строгим правилам в зависимости от типа аккаунта:

Тип аккаунта (AccountType) Шаблон имени Пример
USER_WALLET user.{userId}.{currency} user.1042.THB
TRANSIT system.transit.{channel}.{currency} system.transit.IPPS.THB
REVENUE system.revenue.{currency} system.revenue.THB
NOSTRO system.nostro.{provider}.{currency} system.nostro.ipps.THB
MERCHANT_WALLET merchant.{userId}.{currency} merchant.7.THB
MERCHANT_SETTLEMENT merchant.{userId}.settlement.{currency} merchant.7.settlement.THB
AGENT_WALLET agent.{userId}.{currency} agent.42.THB
AGENT_SETTLEMENT agent.{userId}.settlement.{currency} agent.42.settlement.THB
SERVICE_ACCOUNT service.{userId}.{currency} service.99.THB
EQUITY system.equity.{currency} system.equity.THB

Логика — см. функцию accountName в src/ledger/accounts.ts. Любые отступления от схемы приведут к другому UUID и, как следствие, к «новому» TB-аккаунту.

Transfer ID (input для TRANSFER_NS)

uuidv5TbTransfer(intentId, index)  // input = `${intentId}:${index}`
  • intentId — UUID интента (первичный ключ в pm.intent).
  • index — порядковый номер трансфера внутри одного intent (0, 1, 2, …). У одного intent может быть несколько трансферов: hold, release, fee post, refund и т. д.

Зачем нужны deterministic IDs

1. Verifiability без state

Любой компонент системы, зная имя аккаунта (или пару intentId + index), может вычислить TB ID локально, без обращения к БД PM:

  • Admin Panel запрашивает баланс конкретного мерчанта → вычисляет accountId('merchant.7.THB') и идёт сразу в TigerBeetle.
  • Reconciliation-скрипт сверяет ledger с внешними отчётами PSP — ему достаточно знать intentId и index.

Это резко снижает связанность: маппинг имя → ID хранится только в pm.tb_account_map, но не является источником истины — истина в формуле.

2. Idempotency повторных записей

TigerBeetle при попытке createAccounts / createTransfers с уже существующим ID возвращает статус exists (а не ошибку) и не дублирует запись. Это даёт встроенную идемпотентность на уровне ledger:

  • Повторная попытка создать тот же intent (например, ретрай саги после краша воркера) → тот же uuidv5TbTransfer(intentId, 0) → TB вернёт exists, баланс не сдвинется второй раз.
  • Повторный seedAccounts() при старте PM → каждый системный аккаунт уже создан, TB вернёт exists, всё ok.

Без детерминированных ID пришлось бы реализовывать собственный механизм idempotency-ключей поверх TB (и его согласованность с TB пришлось бы поддерживать вручную).

3. Reconciliation со стороны внешних систем

PSP-адаптеры и Admin Panel могут построить TB ID самостоятельно:

  • Admin Panel читает TigerBeetle напрямую в read-only режиме (см. docs/ADMIN_PANEL.md) — ему не нужен полный доступ к БД PM, достаточно знать имена.
  • Сверочные пайплайны (внутренний аудит, внешний аудитор) проверяют целостность: «вычислил ID из имени → нашёл аккаунт в TB → сравнил баланс с ожидаемым».

Связь с multi-entity accounts (merchant / agent / service)

Все «многосущностные» аккаунты подчиняются той же схеме именования и тем же namespaces:

  • merchant.{userId}.THB — основной merchant wallet.
  • merchant.{userId}.settlement.THB — settlement-аккаунт для выплат.
  • agent.{userId}.THB / agent.{userId}.settlement.THB — то же для агентов.
  • service.{userId}.THB — служебные аккаунты (комиссии партнёров, технические балансы).

Это значит:

  • Auth Center, регистрируя нового мерчанта, не «получает» TB account ID откуда-то — он знает формулу и может проверить существование без вызовов в PM.
  • Новый мерчант с userId = 99 гарантированно получит TB account ID, выводимый из merchant.99.THB. Никаких коллизий с уже существующими аккаунтами — UUID v5 коллизионно-устойчив на размерности 2^128.

См. также: feedback-account-resolution.md в memory (PM никогда не «угадывает» имя — Auth Center передаёт явно).

Запрет: НИКОГДА не менять namespaces

TB_NS и TRANSFER_NSнавсегда фиксированные значения. Любое изменение:

  • Делает невычислимыми все ранее созданные TB account ID и transfer ID из их человекочитаемых ключей.
  • Ломает все существующие записи в pm.tb_account_map, в TigerBeetle, в логах, в outbox.
  • Ломает idempotency: повторная попытка интента создаст новый transfer ID → двойное списание.
  • Ломает Admin Panel и любые внешние верификаторы.

Если когда-либо понадобится «версионирование» схемы ID — это делается через расширение input-строки (например, v2:user.1042.THB), а не через смену namespace. Но в текущей архитектуре такой потребности нет и не предполагается.

В createAccount есть страховочная проверка: id === 0n || id === 2n ** 128n - 1n → выбрасывается ошибка. Это защита от вырожденных входов, но не от смены namespace — последнее ловится только на ревью и при сверке с production-данными.

Связанные документы

  • ../modules/ledger.md — детальный обзор ledger-модуля PM: createAccount, createTransfers, выбор кодов и флагов.
  • ./05-tigerbeetle-accounts.md — типы TB-аккаунтов, коды, флаги (debits_must_not_exceed_credits, history), системные аккаунты, seed-процедура.
  • ./04-two-phase-channels.md — где transfer ID используются в two-phase (pending → post/void) сценариях.