最近、永田町や霞が関から「世界最高水準の決済アーキテクチャ」とか「オンチェーンとオフチェーンの最適な組み合わせ」とか、威勢のいいペーパーが降りてきているらしい。「ガラパゴス化を回避して2030年までにレガシーを抜本アップデートするための7つの方向性」みたいなお題で、「メッセージングとネッティングは誰が担う? A案? D案?」「司令塔はどこだ?」と、各所で議論が始まっているっぽい。
方向性としては全部わかる。わかるんだけど、こういうのって触れるコードが一個あるだけで議論の解像度がぜんぜん変わると思っていて。要件定義のパワポが分厚くなる前に、「とりあえずこんな形になりますけど」という叩き台があると話が早い。
というわけで、待ちきれずに Cloudflare Workers + D1 で「次世代の決済協調層」の参照実装(モック)を、AIにほぼ全任せして個人開発で作ってみた。 自分は「ここはこういう原則で」と方向を投げてレビューしただけ、というのが正直なところ。
- GitHub: pochatt/Zenith-Coordinator (ソース全公開・MITライセンス)
で、作る過程で「お、これは設計判断として面白いな」と思ったところを、自慢半分・共有半分で書いておく。あくまで個人のモックで本番運用は無理なので、そのつもりで読んでほしい。
ちなみに、このシステムの名前は、Zenith Coordinator。AIが名付けただけあって、ちょっと名前負けしているかもしれない(笑)
いちばん「なるほど」だったのは、協調層に金を持たせないこと
ペーパーが求める「モノの引き渡しと支払いを同時に実行するプログラマビリティ」。これを素直に「1対1の銀行振込」の延長でやろうとすると詰む。かといって、N対Mの同時決済やハッシュタイムロック(HTLC)みたいなスマコン機能のために、巨大銀行のレガシー勘定系をフルスクラッチで作り直す? 2030年に間に合うわけがない。
そこで、派手なスマコンの話じゃなくて、「協調層(Zenith / ZC)に一切お金を持たせない」という割り切りをしてみた("マネー非搭載"と呼んでいる)。ZC自身は残高も資金も持たない。ZCが管理するのは 「状態」 と 「送金枠の予約」 だけで、実際の資金は各行の勘定系の中で動く。
具体的にはこう動く。
- ZC側が持つのは H(仕向超過限度=送金側の与信枠) という状態。これは資金そのものではなく、
h_used + amount <= h_limitというカウンタ。送金を受け付けると枠を予約し(H_RESERVED)、決定確定でロック、取消・タイムアウト・DNS清算完了で解放する。コードはsrc/zc/liquidity/h_model.ts。 - 一方、実際の資金の隔離は各行の勘定系の中で起きる。ZCが
reserve-fundsを投げると、支払銀行は顧客口座 −amount / 別段預金(予約口) +amount という内部振替で資金を隔離する(src/bank/ingress.tsのbankReserveFunds、設計上は補遺B.6isolation_mode=SEGREGATED_TRANSFER)。つまりH_RESERVEDという節目では、ZC側の「枠の予約」と、各行側の「別段預金への資金隔離」が両方起きている。
このシステムの特徴的なところは、HTLCの条件付きエスクローを、この別段預金への振替で実現しているところ。lockHtlc(src/zc/lanes/htlc.ts)は H枠を確保したうえで callBankReserveFunds を呼び、資金を別段に寄せてロックする。preimage が出れば別段から支払実行(execute-debit = a 完了)、タイムロック切れなら HTLC_LOCKED → DECIDED_CANCEL(理由コード TIMELOCK_EXPIRED)で別段→顧客口座へ戻入(release-reserve)。ブロックチェーンに資金を載せなくても、既存勘定系が必ず持っている「別段預金」への内部振替だけで条件付きエスクローが成立する——レガシー勘定系のままプログラマビリティを実現する肝はここ。GTID(多者協調)も同じで、登録時に全Payerの資金を別段に Hard Reservation してから一斉に確定する。
逆に 例外なのが高額レーン(HIGH_VALUE) で、こっちは別段預金もHもスキップする。最初は「高額決済ほどa→bを急いで確定させたいからだろう」と思うかもしれないけど、このシステムでは逆を責めた。HIGH_VALUEのa→bは、他レーンよりむしろ遅い——コード中の遷移コメントの通り (a_HV) → waiting for IGS → b で、bを出す前に中央銀行側の決済完了待ちが一手間挟まる。
EXPRESS/STANDARDは、資金を別段預金に寄せたままその場でbを出してしまい、銀行同士で実際にお金を動かす作業(DNS、翌朝のEODバッチでまとめて行うネット決済)は後回しにする。bを出した時点ではまだ銀行間の決済自体は終わっていないので、受け取り側の銀行は「相手行が後でちゃんと払ってくれる」前提でひと足先に確定を出している=立て替えている状態になる。この立て替えの上限が H で、コードのコメントにも"held until DNS settlement" (DNS(日次ネット清算)のリスク管理枠)と明記させた。
高額決済はこの立て替え枠に乗せるには金額が大きすぎる。だからHIGH_VALUEは順番を逆にして、先に銀行間の実際の決済(中央銀行を介したRTGS/IGS)を完了させ、それを確認してからbを出す(bankExecuteDebit のHV分岐は顧客口座から清算専用中継勘定(ZCS)へ直接振替するだけで、別段預金は経由しない)。bを早く出す代わりに立て替えるか、立て替えゼロにする代わりにbを遅らせるか——高額決済は後者を選んでいる、という話。
ポイントは、「ブロックチェーンに資金を載せる」でも「勘定系を作り直す」でもなく、ZCは状態と枠だけ持ち、資金の隔離は各行の別段預金に委ねたこと。プログラマビリティの正体が「別段預金でロック → 協調層が一斉に確定シグナルを出す」ことに還元されるので、勘定系の改修を最小化できる。…という設計をしたつもり。
「オンチェーンとオフチェーンの融合」は、ブリッジを作らないのが正解だった
偉い人たちは「既存インフラと新技術をどう繋ぐか」を議論しがちだけど、システム的に「思想の違うものをブリッジで無理やり繋ぐ」のは一番バグる。
そこでシンプルに、ひとつの状態機械の上に、既存振込(TradFi)もスマコン(DeFi)も対等な「レーン」として並べることをしてみた。橋を架けるんじゃなくて、最初から同じ机に座らせる。
コアは src/zc/orchestrator/state_machine.ts。許可された遷移だけをハードコードして、全部 isValidTransition を通す。
// src/zc/orchestrator/state_machine.ts
export const ALLOWED_TRANSITIONS: Record<TxState, TxState[]> = {
RECEIVED: ["PRECHECKED", "HTLC_LOCKED", "DECIDED_CANCEL"],
PRECHECKED: ["PRECHECKED_SUSPENDED", "H_RESERVED", "DECIDED_CANCEL", "DECIDED_TO_SETTLE"],
H_RESERVED: ["DECIDED_TO_SETTLE", "DECIDED_CANCEL"],
DECIDED_TO_SETTLE: ["PAYER_EXEC_CONFIRMED", "PAYEE_EXEC_CONFIRMED", "SUSPENDED"],
// ...
};
export function isValidTransition(from: TxState, to: TxState): boolean {
return ALLOWED_TRANSITIONS[from]?.includes(to) ?? false;
}
HTLCのプログラマブルな決済も、普通の振り込みのようなネッティングも、全部この同じ関数を通る。「ガラパゴス化を防ぐ相互運用性」って、突き詰めると統一された状態機械を一個作る話だったりする。
「説明責任」はクラウドネイティブでも作り込める
金融インフラで一番こわいのは「お金が宙に浮いて、ログを見ても理由がわからない」状態だと思う。
Zenithでは、状態の UPDATE と監査ログ(FinalityLog)への INSERT を1つのD1バッチでアトミックに実行している(共通ヘルパ transitionWithLog、定義は src/zc/lanes/_helpers.ts。各レーンはこれを呼ぶだけ)。さらにFinalityLogは prev_hash のハッシュチェーンで、日次cronが全チェーンを監査して断絶を検知する。
おかげで 「状態だけ進んで監査ログが残らない隙間」が構造的に存在しない 。トラブル時に誰が照会しても「同じ取引番号で同じ理由コード」が返る。実際にAIにコーディングさせてみて、SQLite(D1)相当の構成でも、設計次第である程度の監査性を担保できる、というのは個人的な発見だった。
一番こだわったのは「どこを過ぎたら取り消せないか」を1点に決めること
決済システムを作るとき、技術より先にハマる問いがある。「お金が戻らなくなる境界線はどこか」。これを曖昧にしたまま実装すると、取消・失敗・誤送金救済の処理がレーンごとに少しずつ違うルールになって、後から「このケースは戻せるの?」を誰も即答できなくなる。
Zenithでは確定点を2段に分けている。a(PAYER_EXEC_CONFIRMED)=送金側の出金確定、b(PAYEE_EXEC_CONFIRMED)=受取側の入金確定(弁済完了)。aはまだ中間点(取消の余地が残る)で、不可逆境界はbの1点に固定した。設計原則としてREADMEにも明記している:
不可逆境界は原則 b(PAYEE_EXEC_CONFIRMED)。b 後の救済は Reversal(別取引)として扱う。
これをただのスローガンにせず、状態機械のレベルで構造的に強制した。ALLOWED_TRANSITIONS を見ると分かりやすい。
PAYEE_EXEC_CONFIRMED: ["SETTLED"],
bに到達した取引が遷移できる先は SETTLED(終端)だけで、DECIDED_CANCEL などキャンセル系の状態には戻る経路がそもそも定義されていない。「bの後にロールバックする」というコードパスは、思想として禁止しているのではなく、状態機械の上に存在しない。
bを過ぎた後に誤りが見つかったらどうするか。取消ではなく、新しい別取引(Reversal)を起票して、逆方向の支払いとして相殺する。 元の取引のレコードは SETTLED のまま一切変更しない。実装は src/zc/cases/reversal.ts に切り出してあり、冒頭のコメントにこの原則をそのまま書いている。
* Implements the spec's Reversal requirement:
* "Cancellation after b (receiving side complete) is prohibited.
* Remedy is performed via Reversal (a separate transaction)."
なぜここまで1点に決め切ることにこだわったかというと、bがブレると他のあらゆる仕組みの土台が揺らぐから。FinalityLogの「同じ取引番号で同じ理由」も、DNS_HOLD時の流動性カスケードも、GTIDの「全legがb一致するまで確定しない」というall-or-nothingも、全部「bがどこか」が一つに定まっていることを前提にしている。 逆に言うと、ここさえ1点に固定できれば、レーンが8種類あっても「結局このお金、戻せるんですか?」に対する答えは常に同じ場所(bを過ぎたか否か)を見れば即答できる。地味だけど、ここが一番ブレてはいけない設計判断だったと思っている。
制度(ルール)も、コードと同じ熱量で書かないと社会に着地しない
ここは作ってみて一番痛感したところ。ペーパーが求める決済インフラは、システム設計だけじゃなく 「危機時の運用ルール」 を言語化しないと現実に降りてこない。「A案かD案か」の前に、やることがある。
なので specs/zenith_policy.md という規程文書もコードと並べて書かせて、AIが迷子にならないようにした。原則のひとつが 「No Heroics(人が無理をして回す設計を禁止する)」。
流動性が枯渇したとき(DNS_HOLD)、運用担当が徹夜でDBを直接叩いてカバーする設計は悪、という立場。Zenithでは未決・不整合はすべて CASE という例外管理状態へシステム的に収束させる。例外処理を運用手順書に逃がさず、 「制度化された状態遷移」 としてコードに埋める。技術仕様だけだと、この辺がすっぽ抜けるんだなと。
正直、ここから先は本職の領域
…と、ペーパーへのエンジニアなりの回答を並べてきたけど、個人のモックでできることの限界もハッキリ見えている。
- 本番運用は一切想定していない。銀行↔協調層は HMAC-SHA256 署名のみで、TLS/mTLS・認証認可・保存時暗号化・規制適合は範囲外。
- 性能値は開発環境の観測で、数千万口座を捌ける証明にはなっていない。
- 仕様には書いたけど未実装の規範要件(トークン化預金の銀行間移転、24/365のDNS複数回化、量子耐性、一般N:M GTIDの多通貨対応など)がまだ普通に残っている。
つまり提言の本丸(量子耐性・24/365・国際接続・運営主体)は、このモックでも未達。そこは正直に認める。
だからこれは「答え」じゃなくて 「叩き台」 。実際の金融インフラを作っている人が見れば「ここの設計が甘い」「これじゃデッドロックする」とすぐ見抜けるはずで、それでいい。むしろそのツッコミがほしくて公開している。
パワポが分厚くなる前に、触れるコードで殴り合えると議論が早い。ソースは全部GitHubに置いてあるので、踏み台にでもしてください。2030年の「次」を、誰かが本物にしてくれたら最高。
Happy Coding.