第12巻 DBスキーマ定義(1) ― マイグレーション運用とZC基本テーブル
目次
- マイグレーション運用
- 鉄則
- ZC基本テーブル
- Participants(参加主体)
- Transactions(取引)
- HReservations(H予約)
- ParticipantCurrencyLimits(非JPYのH上限/使用量)
- FinalityLog(不変ログ・INSERT ONLY、改ざん耐性ハッシュチェーン)
- FinalitySeq(FinalityLog 単調 event_seq 採番)
- EntityStateLog(非Transaction エンティティの状態遷移履歴・INSERT ONLY)
- DnsCycles(DNSサイクル)
- IgsDeferQueue(IGS Deferキュー — DNS HOLD中の優先度付き再投入)
- IgsThrottleState(IGS公平性スロットル消費)
- LsmRuns(Bulk LSM最適化の採択証跡)
- DnsNetPositions(DNS清算明細)
- HtlcContracts(HTLC取引)
- GtidTransactions(GTID取引)
- GtidLegs(GTIDの脚)
- FxQuotes(FXプロバイダの方向別レート)
- FxTransfers(FX送金レコード)
- FxLegLocks(FX脚のHTLCロック)
- Cases(例外処理チケット)
- CaseRelatedTransactions(集約 CASE に束ねた取引・INSERT ONLY)
- AccessAuditLog(アクセス監査台帳・INSERT ONLY)
- Vault(短期秘匿ストア)
- PsprRegistry(PSPR登録)
- RtpRequests(RTP請求)
- IdempotencyKeys(冪等キー管理:ZC側)
第12巻 DBスキーマ定義(1) ― マイグレーション運用とZC基本テーブル
本巻は
docs/specs/31_schema.mdのマイグレーション運用〜ZC基本テーブルを収める。続きは→第13巻。
migrations/ のSQLと完全に一致させること。矛盾がある場合は migrations/0001_consolidated_schema.sql(SQL)を正とし、本ファイルを追随修正する。
マイグレーション構成:
0001_consolidated_schema.sql— 統合スキーマ(唯一の正)。 かつては
旧0001〜0042の連番マイグレーション(ZC基本テーブル / Bank基本テーブル /
トレーサビリティ・着金フィルタ・HTLC Auth / 新決済機能 / RTP / BOJプレファンド /
DNS / Circuit Breaker / Reversal / FinalityLog ハッシュチェーン /
EntityStateLog / KeyRegistry / Attestation / Mandate / クロスチェーンHTLC /
マルチ通貨 / 稼働ウィンドウ / 給付行政 / DNS リングフェンス 等)が存在し、
後続でさらに0002〜0006(BankJournalsの通貨次元化 /DnsCyclesの
決済チェーン / クロスカレンシー FX マーケットプレイス)も切られていた。
本ファイルは、それら増分のALTER/DROP/RENAME履歴をすべて畳み込み、
最終形の 1 ファイルへ統合したものである。全テーブルを確定形で定義し、
索引と初期シードデータ(システム参加者・システム勘定・プレファンド仕訳・
金利・FinalitySeq・SystemMode)を含む。
統合に含まれる主な最終形:BankJournals.amount_currency(ISO 4217、DEFAULT 'JPY')+ 索引idx_jnl_account_ccy。元帳金額の通貨次元化(共有の透明・別段勘定をまたぐ
単位混在の防止)。DnsCycles.settlement_chain。非JPYサイクルが確定するトークン化中銀当座
(CBT)のチェーンを固定(NULL=JPYクラシックBOJ-Net)。通貨→中央銀行の
レジストリはsrc/shared/central_bank.ts。Participants.is_fx_provider+FxQuotes表(索引idx_fxq_pair/idx_fxq_fxp)。FXプロバイダ(FXP)が提示する方向別レート。レートは整数
固定小数(RATE_SCALE=1e8)。クロスカレンシー FX(docs/specs/30_internal_design.md)の第1段。FxTransfers表。FX送金1件の事実(採用経路・ロックした見積・実効レート・
脚を束ねる共有ハッシュロック)を gtid 単位で保持。FX送金は FXP導管型 GTID
(payer→FXP→payee の通貨別脚)として既存 GTID レーンで原子決済される
(src/zc/fx/transfer.ts)。FxLegLocks表(索引idx_fxleglocks_state)。クロスレール原子性
(30_internal_design.md(FX内部設計・実装状況))の束ねレイヤ。共有ハッシュロック+段階的タイムロックで
各通貨脚をロックし、secret 公開(claim)で全脚を一括 CLAIMED にして初めて
GTID を登録・決済、未公開ならタイムロック満了で全脚 REFUNDED(無決済)。
実装はsrc/zc/fx/htlc.ts。
過去の連番マイグレーションについて: かつて存在した
0002〜0042の個別ファイル(および本番デプロイ後に判明した不具合のパッチ群)、ならびに
後続で切られた
0002〜0006の各ファイルは、本統合にあたって削除済み。新規 DB はこの 1 ファイルを適用すれば完全なスキーマが得られる。
スキーマの正は本ファイル (
31_schema.md) とmigrations/0001_consolidated_schema.sqlであり、齟齬が出たら SQL 側を信じる。
マイグレーション運用
鉄則
- 統合スキーマ (
0001_consolidated_schema.sql) を直接編集する。
本リポジトリはスキーマを単一の統合ファイルで保持する参照実装であり、
スキーマ変更は新しい連番ファイルを切らず、この 1 ファイルを唯一の正と
して直接書き換える。31_schema.md(本ファイル)も必ず同じ変更で更新し、
両者を一致させる。- 列の追加・変更は対象
CREATE TABLEの定義に直接列を足す(別の
後置ALTERを積まない)。確定形の 1 ファイルを保つ。 - 本番運用上の注意: D1 の
wrangler d1 migrations applyは適用済み
ファイルの再適用を行わないため、すでにデプロイ済みの実 DB に同手法を
適用する場合は別途の前進的マイグレーション(または再構築)が要る。
本参照実装は「新規 DB にこの 1 ファイルを適用して完全形を得る」前提。
- 列の追加・変更は対象
test/helpers/d1-mock.tsのSCHEMA_MIGRATIONSは0001_consolidated_schema.sql単体を保つ。 テスト/ローカルは統合
スキーマを毎回新規適用するため、スキーマ変更は同ファイルへの編集だけで
反映される(配列に新ファイルを足さない)。- インデックスは追加したら
31_schema.mdの索引カタログにも必ず記載する。
未記載はtest/invariants/schema_doc_drift.test.tsが検出する。
ZC基本テーブル
Participants(参加主体)
CREATE TABLE Participants (
bank_id TEXT PRIMARY KEY, -- '001', '002', ...
bank_name TEXT NOT NULL,
ingress_base_url TEXT NOT NULL, -- '/bank/001'
h_limit INTEGER NOT NULL DEFAULT 0, -- H上限(円)
h_used INTEGER NOT NULL DEFAULT 0, -- H消費中(円)
is_active INTEGER NOT NULL DEFAULT 1,
registered_at TEXT NOT NULL, -- RFC3339
participation_mode TEXT NOT NULL DEFAULT 'FULL', -- FULL|RECEIVE_ONLY|SEND_ONLY
tx_amount_limit INTEGER, -- 1件あたり上限(円)
daily_amount_limit INTEGER, -- 日次上限(円)
daily_amount_used INTEGER NOT NULL DEFAULT 0, -- 日次累計(EODリセット)
-- 日次上限のリセット日付(クロン未実行時の自動リセット判定用)
daily_amount_last_reset_date TEXT, -- 'YYYY-MM-DD'
-- HIGH_VALUE 自動エスカレーション閾値(制度パラメータ PR-HV-THRESHOLD、30_internal_design.md §12.9)
-- NULL = 環境変数 ZC_HV_THRESHOLD(既定 1 億円)にフォールバック
hv_threshold INTEGER,
-- 参加行の稼働ウィンドウ('HH:MM' JST=システム時刻)
-- NULL/NULL = 常時オープン
-- start < end: 同日内ウィンドウ / start > end: 日付をまたぐウィンドウ
-- start === end: 24時間オープン扱い
operating_window_start TEXT,
operating_window_end TEXT,
-- 給付発起参加者の類型化
participant_type TEXT NOT NULL DEFAULT 'BANK', -- 'BANK'|'GOVERNMENT'
-- FXプロバイダ(FXP)フラグ。is_fx_provider=1 かつ 2通貨以上の
-- ParticipantCurrencyLimits 行を持つ参加行は、その通貨ペアの方向別 FxQuotes を提示できる。
is_fx_provider INTEGER NOT NULL DEFAULT 0
);
h_used/daily_amount_usedの設計意図: これらは一見すると
SUM(HReservations WHERE is_released=0)や当日Transactionsの合計から導出できる冗長カラムに見えるが、意図的に保持している実体化カウンタで
ある。
UPDATE Participants SET h_used = h_used + ? WHERE (h_used + ?) <= h_limitのような単文 UPDATE は、上限チェックと加算を 1 命令で済ませることで同時実行下でも race-free に上限を強制できる。SUM して比較してから
INSERT する形に置き換えると、SUM と INSERT の間に TOCTOU 窓が開いて
上限超過の二重予約が発生し得る。本リポジトリは「事実は追記、状態カラムは
更新しない」を原則とするが、性能/同時実行のための materialization は
例外として明示的に容認する(参照:
src/zc/liquidity/h_model.ts#reserveH、
src/zc/ingress/transfers.tsのdaily_amount_used加算)。整合性はis_released=0な
HReservationsの合計との reconciliation で随時検証できる。
Transactions(取引)
CREATE TABLE Transactions (
txid TEXT PRIMARY KEY,
lane TEXT NOT NULL, -- EXPRESS|STANDARD|BULK|DEFERRED|RTP|HTLC|HIGH_VALUE|GTID|DIRECT_DEBIT
state TEXT NOT NULL, -- TxState
amount_value INTEGER NOT NULL,
amount_currency TEXT NOT NULL DEFAULT 'JPY',
payer_bank_id TEXT NOT NULL,
payer_account_hash TEXT NOT NULL,
payee_bank_id TEXT NOT NULL,
payee_account_hash TEXT,
pspr_ref TEXT,
purpose TEXT, -- MERCHANT|P2P|BILL|SALARY|REFUND
idempotency_key TEXT UNIQUE NOT NULL,
schema_version TEXT NOT NULL DEFAULT '1.0',
h_reservation_id TEXT, -- FK → HReservations
decision_proof_ref TEXT,
finality_log_ref TEXT,
payer_bank_proof_ref TEXT, -- JSON: bank_proof_ref構造
payee_bank_proof_ref TEXT, -- JSON: bank_proof_ref構造
reason_code TEXT,
case_id TEXT,
dns_cycle_id TEXT,
expires_at TEXT, -- RFC3339
version INTEGER NOT NULL DEFAULT 0, -- 楽観的ロック
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
external_settlement_status TEXT DEFAULT 'NONE', -- NONE|REQUESTED|SETTLED|FAILED|HOLD
verification_id TEXT, -- FK → AccountVerifications
edi_ref TEXT, -- FK → EdiRecords
fatf_data_json TEXT, -- FATF R16データ JSON
is_cross_border INTEGER NOT NULL DEFAULT 0,
fatf16_applicable INTEGER NOT NULL DEFAULT 0,
mandate_id TEXT, -- FK → Mandate(任意)
-- 相手行の稼働ウィンドウ待ちで
-- PRECHECKED_SUSPENDED + reason_code='COUNTERPARTY_WINDOW_CLOSED' に
-- 一時停止した際の元PaymentInitiatedRequestのスナップショット(JSON)。
-- タイムアウトスイープがウィンドウ再オープン後にこれを復元してEXPRESSを再開する。
pending_request_json TEXT,
-- 単一所有者則: この行を「いま動かせる」唯一の主体。
-- 'ZC' | 'CYCLE:<cycle_id>' | 'VENUE:<venue_id>' | 'CHAIN:<watcher_set>'
owner TEXT NOT NULL DEFAULT 'ZC',
-- timeout sweep が計測する「待ちの開始時刻」。書き込むのはレーン共通
-- プリミティブ(insertTxWithLog / transitionWithLog)と Authority Check の
-- 留置印(_authority_check.ts)だけである。
--
-- なぜ updated_at ではないか: updated_at はこの行への**あらゆる**書込みで動く。
-- 進捗と無関係な書込み——CASE 起票時の case_id(src/zc/cases/case.ts)、
-- 送金内容データ連携時の edi_ref(src/zc/richdata/edi.ts)——にも状態ガードが
-- 無いため、滞留した取引に CASE を起票するという運用者の当然の動作が、
-- その取引自身の期限を後ろへずらしていた。繰り返せば無限にずれる。
-- 有界時間内の検出(docs/disclosure/CORE_DISCLOSURE.md【0013】(a)・【0124】4)が
-- 成立しない。タイマを最も必要とする行ほど、待っている間に別の理由で
-- 触られる機会が多いからである。
--
-- NULL は本列の導入前に書かれた行のみ。sweep は
-- COALESCE(pending_since, updated_at) を読むので、それらも従来どおり期限切れする。
pending_since TEXT
);
CREATE INDEX idx_tx_state ON Transactions(state);
CREATE INDEX idx_tx_owner ON Transactions(owner, state);
CREATE INDEX idx_tx_payer ON Transactions(payer_bank_id, state);
CREATE INDEX idx_tx_payee ON Transactions(payee_bank_id, state);
CREATE INDEX idx_tx_dns ON Transactions(dns_cycle_id);
CREATE INDEX idx_transactions_mandate_id ON Transactions(mandate_id);
laneの値域(規範): 本列は 8 値(EXPRESS|STANDARD|BULK|DEFERRED|RTP|HTLC|HIGH_VALUE|GTID)。このうち
GTIDは API リクエスト値ではない ——POST /api/transfersが受け付ける 7 値(
32_api_contracts.md § POST /api/transfers、実装src/types/states.ts#LaneType)に加え、GT-level Decision 確定後にレッグを実体化する際
src/zc/lanes/gtid/advance.tsが内部生成する。列の値域は
src/types/states.ts#TxLane。
HTLC_AUTHは本列の値ではない:受取側起点オーソリはlane='HTLC'+ FinalityLog payload の
flow='HTLC_AUTH'として表現し、詳細はHtlcAuthRequestsに持つ(用語の切り分けは
10_requirements.md序章「レーンとlane列の関係」を正とする)。
owner(単一所有者則): どの瞬間も各取引を動かす権利はちょうど一人が持つ(単一所有者則の詳細は
30_internal_design.md#single-owner)。所有権の移転は
transferOwnership/transitionWithLogのsetColumns(
src/zc/lanes/_helpers.ts)に一本化され、FinalityLog に
OwnershipTransferred/OwnershipReclaimedとして記録される。タイムアウトスイープは
owner='ZC'の行だけを対象とする(
dns_cycle_id/external_settlement_statusによる除外述語の列挙を置換)。
HReservations(H予約)
CREATE TABLE HReservations (
reservation_id TEXT PRIMARY KEY,
txid TEXT NOT NULL,
bank_id TEXT NOT NULL,
amount INTEGER NOT NULL,
mode TEXT NOT NULL DEFAULT 'RESERVED', -- RESERVED|LOCKED
is_released INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
released_at TEXT,
-- このリザベーションが消費するH容量の通貨。
-- 'JPY' は Participants.h_limit/h_used、それ以外は ParticipantCurrencyLimits を消費する。
currency TEXT NOT NULL DEFAULT 'JPY'
);
CREATE INDEX idx_hres_bank ON HReservations(bank_id, is_released);
ParticipantCurrencyLimits(非JPYのH上限/使用量)
-- 通貨を H-Model の次元に昇格。JPY は引き続き Participants.h_limit/h_used を
-- 使用し、非JPY通貨はこのテーブルの (bank_id, currency) 単位の行で
-- 上限・使用量を管理する。既存JPY専用フローへの影響・データ移行は無し。
CREATE TABLE ParticipantCurrencyLimits (
bank_id TEXT NOT NULL,
currency TEXT NOT NULL,
h_limit INTEGER NOT NULL DEFAULT 0,
h_used INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (bank_id, currency)
);
FinalityLog(不変ログ・INSERT ONLY、改ざん耐性ハッシュチェーン)
CREATE TABLE FinalityLog (
log_id TEXT PRIMARY KEY, -- UUID
txid TEXT,
gtid TEXT,
event_type TEXT NOT NULL, -- 監査イベント名。値域は src/types/api/messaging.ts#FinalityEventType(I/F語彙との関係は 30_internal_design.md §12.1.6)
state_from TEXT,
state_to TEXT NOT NULL,
payload_json TEXT NOT NULL, -- イベント全体(JSON)
event_seq INTEGER NOT NULL, -- FinalitySeq から単調割当
occurred_at TEXT NOT NULL,
-- SHA-256 ハッシュチェーン
prev_hash TEXT, -- 同 chain の直前 entry_hash(先頭は 'GENESIS')
entry_hash TEXT -- SHA-256(prev|log_id|txid|gtid|event_type|state_from|state_to|payload_json|event_seq|occurred_at)
);
CREATE INDEX idx_fl_txid ON FinalityLog(txid);
CREATE INDEX idx_fl_gtid ON FinalityLog(gtid);
CREATE INDEX idx_fl_seq ON FinalityLog(event_seq);
CREATE INDEX idx_fl_chain_seq ON FinalityLog(txid, event_seq);
CREATE INDEX idx_fl_gchain_seq ON FinalityLog(gtid, event_seq);
-- TX チェーンの prev_hash 部分 UNIQUE(並列ワーカーによる分岐防止)
CREATE UNIQUE INDEX idx_fl_chain_prev_hash
ON FinalityLog(txid, prev_hash) WHERE txid IS NOT NULL;
-- event_seq 全体 UNIQUE(FinalitySeq の belt-and-braces)
CREATE UNIQUE INDEX idx_fl_event_seq_unique ON FinalityLog(event_seq);
-- GTID 専用チェーンの prev_hash 部分 UNIQUE
CREATE UNIQUE INDEX idx_fl_gtid_chain_prev_hash
ON FinalityLog(gtid, prev_hash) WHERE gtid IS NOT NULL AND txid IS NULL;
ハッシュチェーンの規範
- chain_id:
COALESCE(txid, gtid, 'GLOBAL')で識別される。TX と GTID
は独立したチェーンに記録され、'GLOBAL'はシステム全体イベント用。 - prev_hash の決定: 同一 chain の直前エントリの
entry_hash。新規
チェーン先頭は'GENESIS'。 - entry_hash の決定: 上記 SQL コメント記載の通り、フィールドを
|
連結した文字列を SHA-256。フィールド順は契約。 - 検証:
verifyChain(db, chain_id)がチェーン全体を再計算し、LEGACY_UNCHAINED_ENTRY/PREV_HASH_MISMATCH/ENTRY_HASH_MISMATCH
のいずれかを break_reason として返す。GET /api/transactions/:txid/verify
およびGET /api/gtid/:gtid/verify経由で公開。 - 書込み原子性:
transitionWithLogは CAS UPDATE と FinalityLog
INSERT を 1 つのdb.batch()で発行し、changes() > 0でガードする。
CAS に勝った呼び出しのみがログを書く。
FinalitySeq(FinalityLog 単調 event_seq 採番)
CREATE TABLE FinalitySeq (
id INTEGER PRIMARY KEY CHECK (id = 1),
next_seq INTEGER NOT NULL
);
-- 初期化(migration 内で実行)。統合スキーマを適用する新規DBでは FinalityLog が
-- 空のため、next_seq はリテラル 0 をシードする(統合スキーマの確定形)。
INSERT OR IGNORE INTO FinalitySeq (id, next_seq)
VALUES (1, 0);
writeFinalityLog は UPDATE FinalitySeq SET next_seq = next_seq + 1 WHERE id = 1 RETURNING next_seq で event_seq をアトミック割当する。
補足(シード値について): 既存ログを持つ実 DB へ後追いで適用する場合は
COALESCE((SELECT MAX(event_seq) FROM FinalityLog), 0)で現在値から再開させるのが安全だが、統合スキーマは「新規 DB にこの 1 ファイルを適用して完全形を得る」前提であり
FinalityLogが空なので、(1, 0)のリテラル値をシードする。
EntityStateLog(非Transaction エンティティの状態遷移履歴・INSERT ONLY)
マネーパス(Transactions / HtlcContracts / GtidTransactions / GtidLegs)
の状態遷移は transitionWithLog が FinalityLog に、DNS サイクルは'DNS-' チェーンにそれぞれ追記する。一方で運用系エンティティ
(Cases.state / PsprRegistry.capability_state / BankAccounts.status /ReversalRecords.status)は status 列を上書きするだけで遷移履歴が失われて
いた。EntityStateLog は status 列を現在状態の射影として残しつつ、変更
ごとに不変の事実(state_from → state_to)を 1 行追記する。UPDATE/DELETE
は一切しない。
CREATE TABLE EntityStateLog (
log_id TEXT PRIMARY KEY, -- 'ESL-<uuid>'
entity_type TEXT NOT NULL, -- 'CASE'|'PSPR'|'BANK_ACCOUNT'|'REVERSAL'|'MANDATE'|'DEBIT_MANDATE'|'COLLECTION'
entity_id TEXT NOT NULL, -- エンティティ主キー値
event_type TEXT NOT NULL, -- ドメインイベント名('CaseOpened' 等)
state_from TEXT, -- 直前状態(生成時 NULL)
state_to TEXT NOT NULL, -- 新状態
reason_code TEXT, -- 変更理由(任意)
actor TEXT, -- 'ZC'|'OPS'|'BANK_{bankId}'|'SYSTEM'
payload_json TEXT, -- 追加コンテキスト JSON(任意)
occurred_at TEXT NOT NULL -- RFC3339
);
CREATE INDEX idx_esl_entity ON EntityStateLog(entity_type, entity_id, occurred_at);
CREATE INDEX idx_esl_occurred ON EntityStateLog(occurred_at);
書込み規範: src/shared/entity_state_log.ts#transitionEntityWithLog が
status 変更 UPDATE と EntityStateLog INSERT を 1 つの db.batch() で発行する。
INSERT は直前 UPDATE の changes() > 0 をガードに用いる条件付き形式のため、
no-op(同一状態への再適用など)ではログを書かない。FinalityLog のbuildFinalityLogConditionalInsert と同型。同一 occurred_at のタイ解決は
INSERT 順(rowid 昇順)で行う。
DnsCycles(DNSサイクル)
business_date には UNIQUE 制約を貼らない(late-arriving cycle が同一
business_dateで複数サイクルを持てるよう、suffix付きcycle_idで区別する)。
currency / intraday_seq 列は、通貨別・日内複数回サイクルを一意化する
正規識別子形式 DNS-{CCY}-YYYYMMDD-NN(src/zc/settlement/dns_cycle_id.ts)の
識別情報を保持する列である。この2列は cycle_id
文字列(DNS-${business_date} / late-cycle の DNS-${business_date}-${HHMMSS})
とは独立して管理される。単一日次 JPY サイクルは currency='JPY',intraday_seq=1(正規形 DNS-JPY-YYYYMMDD-01 相当)として記録される
(src/zc/settlement/dns_cycle_id.ts の legacyDnsCycleIdentity())。late-cycle には
その business_date 内での作成順に基づく連番(2, 3, ...)が割り当てられる。
CREATE TABLE DnsCycles (
cycle_id TEXT PRIMARY KEY,
business_date TEXT NOT NULL, -- 'YYYY-MM-DD'(UNIQUEではない。複数サイクル/日を許容)
state TEXT NOT NULL DEFAULT 'OPEN', -- DnsState: OPEN|KICKED|SETTLED|HOLD_ACTIVE
igs_mode TEXT NOT NULL DEFAULT 'NORMAL', -- IgsMode: NORMAL|STOP|RINGFENCED|RINGFENCED_PLUS
kicked_at TEXT,
settled_at TEXT,
hold_reason TEXT, -- JSON: {reason, shortfalls}(`shortfalls` は閉域情報。`20_method_design.md` §9.4.4)
net_positions TEXT, -- JSON: {bank_id: net_amount}
updated_at TEXT, -- holdDnsが参照
currency TEXT NOT NULL DEFAULT 'JPY', -- 正規識別子のCCY(`20_method_design.md` §9.4.2)
intraday_seq INTEGER NOT NULL DEFAULT 1, -- 正規識別子のNN(`20_method_design.md` §9.4.2)
created_at TEXT NOT NULL,
hold_causing_participants TEXT, -- JSON配列: HOLD時にショートフォールの原因となった参加行集合(`20_method_design.md` §2.4 類型B のリングフェンス、settleDnsがRINGFENCED昇格時に記録、settle成功でNULLクリア)
-- RINGFENCED_PLUS 昇格証跡(`20_method_design.md` §2.4 類型B・§10.9.3.1)。ZCが算式で
-- リアルタイム算定する復旧リザーブと、その再現可能性のための入力/算式/出力
-- ダイジェスト(reserve_explain_hash)・信頼度(reserve_confidence)。
-- promoteRingfencePlus が RINGFENCED→RINGFENCED_PLUS 昇格時に記録する。
dns_recovery_reserve INTEGER, -- 算定リザーブ
reserve_explain_hash TEXT, -- 入力+算式+出力のSHA-256
reserve_confidence REAL, -- 信頼度 0..1(閾値以上で昇格)
settlement_chain TEXT, -- 決済レール/チェーン。NULL=JPYクラシックBOJ-Net({bank}-BOJ)。非JPYは 'ETH'|'POLYGON'|… を固定し、トークン化中銀当座 {bank}-CBT-{CCY}-{CHAIN} で確定
-- HOLD 中の公式発表テンプレID(`DNS_HOLD_{business_date}`)。HOLD_ACTIVE 以外は NULL。
-- ZC の公式ステータスと、参加行が顧客へ表示してよい定型文を機械的に突合するための
-- 唯一のキー(`10_requirements.md` §3.3.1-3/-4、`20_method_design.md` §9.4.4.2)。
public_message_id TEXT
);
CREATE INDEX idx_dns_business_date ON DnsCycles(business_date); -- 非UNIQUE
中銀決済レール(settlement_chain): ファイナリティはその通貨の中央銀行で
確定する。BOJ は JPY のみを扱い、EUR は ECB、USD は FedNY… となる。ZC は日本の
コーディネータで外国中銀の RTGS に直接接続できないため、非JPYはトークン化中銀
当座預金(CBT)をチェーン上で確定させる:cycle がsettlement_chainを固定し、決済は
{bank}-CBT-{CCY}-{CHAIN}勘定(発行中銀)へ向かう。確定の証跡は発行体署名を
KeyRegistryで検証したvenue='CB_TOKEN'のSettlementProofRef(ONCHAIN /ATTESTATION と同じ信頼モデル)。JPY のクラシック BOJ 当座(
{bank}-BOJ)は不変で、トークン化JPY(
{bank}-CBT-JPY-{CHAIN})は追加レール。詳細は
src/shared/central_bank.ts。
IgsDeferQueue(IGS Deferキュー — DNS HOLD中の優先度付き再投入)
DNS HOLD(igs_mode)中にブロックされた IGS(HIGH_VALUE)を、単純な
suspend→sweep ではなく優先度+実行予定ウィンドウ付きで再投入するキュー
(20_method_design.md §2.4 類型B・§10.9.3.1「拒否ではなく Defer を原則とし、scheduled_execution_window を付与する」)。txid 単位で一意(idx_igs_defer_txid)。
timeout sweep が status='DEFERRED' かつ実行ウィンドウ到来分を priority 昇順で
再投入する。
CREATE TABLE IgsDeferQueue (
defer_id TEXT PRIMARY KEY,
txid TEXT NOT NULL,
cycle_id TEXT NOT NULL,
payer_bank_id TEXT NOT NULL,
payee_bank_id TEXT NOT NULL,
amount_value INTEGER NOT NULL,
reason_code TEXT NOT NULL, -- DNS_IGS_THROTTLED|DNS_RINGFENCED|DNS_HOLD_IGS_STOPPED
priority INTEGER NOT NULL DEFAULT 100, -- 小さいほど優先
scheduled_execution_window TEXT NOT NULL, -- ISO: この時刻以降で再投入可
status TEXT NOT NULL DEFAULT 'DEFERRED', -- DEFERRED|RESUMED|CANCELLED
enqueued_at TEXT NOT NULL,
resumed_at TEXT
);
CREATE INDEX idx_igs_defer_status ON IgsDeferQueue(status, priority, scheduled_execution_window);
CREATE UNIQUE INDEX idx_igs_defer_txid ON IgsDeferQueue(txid);
IgsThrottleState(IGS公平性スロットル消費)
RINGFENCED_PLUS 中に各参加行が消費した IGS admission 予算
(igs_throttle_budget 公平性制御。20_method_design.md §2.4 類型B)。予算超過分は reject ではなく Defer
され、希少な復旧期流動性を一行が独占しないようにする。
CREATE TABLE IgsThrottleState (
cycle_id TEXT NOT NULL,
bank_id TEXT NOT NULL,
admitted_amount INTEGER NOT NULL DEFAULT 0,
admitted_count INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
PRIMARY KEY (cycle_id, bank_id)
);
LsmRuns(Bulk LSM最適化の採択証跡)
Bulk/Deferred の LSM(流動性節約)最適化の各ウィンドウ実行を記録する監査表
(30_internal_design.md §14.2 / §14.3)。入力スナップショット・制約ダイジェスト・採択集合ハッシュ・
追跡ダイジェスト・目的関数の達成度(およびフォールバック時の劣化)を残し、
「なぜこの集合が採択されたか」を後から説明可能にする。
CREATE TABLE LsmRuns (
run_id TEXT PRIMARY KEY,
business_date TEXT NOT NULL,
window_id TEXT NOT NULL,
mode TEXT NOT NULL, -- OPTIMIZED|FIFO|PRIORITY|THROTTLE
is_fallback INTEGER NOT NULL DEFAULT 0,
input_snapshot_id TEXT NOT NULL, -- 候補集合ダイジェスト(F.2)
constraints_digest TEXT NOT NULL, -- H残高/期限/優先度/停止条件(F.2)
execution_set_hash TEXT NOT NULL, -- 採択tx集合(F.2)
trace_digest TEXT NOT NULL, -- 再現可能な根拠(F.2)
candidate_count INTEGER NOT NULL DEFAULT 0,
selected_count INTEGER NOT NULL DEFAULT 0,
deferred_count INTEGER NOT NULL DEFAULT 0,
objective_metrics TEXT NOT NULL, -- 辞書式目的の達成度+劣化(F.1/F.3)
created_at TEXT NOT NULL
);
CREATE INDEX idx_lsm_runs_date ON LsmRuns(business_date, created_at);
DnsNetPositions(DNS清算明細)
CREATE TABLE DnsNetPositions (
id TEXT PRIMARY KEY,
cycle_id TEXT NOT NULL,
bank_id TEXT NOT NULL,
gross_send INTEGER NOT NULL DEFAULT 0,
gross_receive INTEGER NOT NULL DEFAULT 0,
net_position INTEGER NOT NULL DEFAULT 0, -- 正=受取超、負=支払超
is_settled INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (cycle_id) REFERENCES DnsCycles(cycle_id)
);
HtlcContracts(HTLC取引)
CREATE TABLE HtlcContracts (
htlc_id TEXT PRIMARY KEY,
txid TEXT NOT NULL UNIQUE,
state TEXT NOT NULL, -- HtlcState
hashlock TEXT NOT NULL, -- SHA256ハッシュ(hex)
timelock TEXT NOT NULL, -- RFC3339(期限。ZC側の外側タイムロック)
amount_value INTEGER NOT NULL,
payer_bank_id TEXT NOT NULL,
payee_bank_id TEXT NOT NULL,
secret_verified INTEGER NOT NULL DEFAULT 0, -- 1=検証済み
authority_recheck_required INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
-- クロスチェーンHTLC関連列
cross_chain_source TEXT, -- NULL=通常のHTLC。非NULLの場合、同じhashlockが
-- この`source`下のオンチェーンエスクローもロックする
onchain_timelock TEXT, -- RFC3339(オンチェーン側の内側タイムロック。
-- 必ず`timelock`より厳密に前)
onchain_lock_ref TEXT, -- CrossChainLocked観測のexternal_ref
onchain_lock_proof_json TEXT, -- CrossChainLocked観測のSettlementProofRef(JSON)
onchain_release_proof_json TEXT, -- OnchainProofObserved観測のSettlementProofRef(JSON)
-- 条件テンプレート参照(プログラマビリティの汎用化)
condition_template_id TEXT, -- FK → ConditionTemplate(任意)。設定時は
-- claimHtlcByAttestationによるアテステーション
-- 経由の成立(PASS)でも、既存のpreimage経由でも
-- fulfillできる
onchain_min_confirmations INTEGER NOT NULL DEFAULT 0, -- オンチェーン解放を確定とみなす最小承認数(確認深度ゲート)
onchain_min_watchers INTEGER NOT NULL DEFAULT 1, -- 決済に必要な相異なる Watcher 運用主体の数(n-of-m クォーラム。1=従来の単一Watcher)
-- AND/OR 条件合成(プログラマビリティの汎用化)
condition_expr_json TEXT, -- {template_id} | {op:AND|OR, operands:[...]} の式木。claimHtlcByConditions で評価
-- クロスチェーン確定種別 + 量子リスク証跡
onchain_chain_class TEXT, -- PUBLIC|PRIVATE|PERMISSIONED
onchain_crypto_suite TEXT, -- 例: secp256k1|ed25519|dilithium3
onchain_quantum_risk TEXT, -- VULNERABLE|RESISTANT|UNKNOWN(suite から導出可)
onchain_finality_class TEXT, -- PROBABILISTIC|DETERMINISTIC(chain_class から導出)
FOREIGN KEY (txid) REFERENCES Transactions(txid)
);
GtidTransactions(GTID取引)
legs_ready_count/legs_settled_countの位置付け: GT の合流判定(
checkAndFinalizeGtid)は実 leg/tx state を JOIN して評価しており、これらのカラムを参照していない。ダッシュボード表示用の denormalize 値で
あり、
GT_DECIDED_TO_SETTLE/GT_SETTLED遷移時に snapshot 書込みされるため終端状態では正確だが、原理的には drift し得る。単一 GTID 詳細 API
(
handleGetGtid)は実 leg 状態から導出した値で上書きして返す。新規のGT 合流ロジックは実 leg state を参照し、これらのカウンタに依存しない
こと(参照:
src/zc/orchestrator/gtid.ts#checkAndFinalizeGtid)。
CREATE TABLE GtidTransactions (
gtid TEXT PRIMARY KEY,
state TEXT NOT NULL, -- GtidState
initiator_bank_id TEXT NOT NULL,
total_amount INTEGER NOT NULL,
leg_count INTEGER NOT NULL,
legs_ready_count INTEGER NOT NULL DEFAULT 0,
legs_settled_count INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
version INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
mandate_id TEXT -- FK → Mandate(任意)
);
GtidLegs(GTIDの脚)
CREATE TABLE GtidLegs (
leg_id TEXT PRIMARY KEY,
gtid TEXT NOT NULL,
txid TEXT, -- 紐付くtxid(DECIDED後に確定)
role TEXT NOT NULL, -- PAYER|PAYEE
bank_id TEXT NOT NULL,
account_hash TEXT NOT NULL,
amount_value INTEGER NOT NULL,
state TEXT NOT NULL, -- LegState
bank_proof_ref TEXT, -- JSON
expires_at TEXT,
version INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
-- 脚ごとの通貨。PvP(多通貨同時決済)は
-- 「異通貨2レッグの GTID」として、このカラムを参照して脚をグルーピングする。
leg_currency TEXT NOT NULL DEFAULT 'JPY',
-- 受付時の正規化(`20_method_design.md` §2.2.5.1)が leg_id を書き換えた場合の、
-- 参加者が登録した元の leg_id。NULL = 登録どおりの脚。
origin_leg_id TEXT,
FOREIGN KEY (gtid) REFERENCES GtidTransactions(gtid),
FOREIGN KEY (txid) REFERENCES Transactions(txid)
);
CREATE INDEX idx_legs_gtid ON GtidLegs(gtid);
CREATE INDEX idx_legs_txid ON GtidLegs(txid); -- orchestrator.ts の txid 検索(onPayeeExecConfirmed/suspendTx)のフルスキャン対策
FxQuotes(FXプロバイダの方向別レート)
クロスカレンシー FX(docs/specs/30_internal_design.md)のレート市場。FXP(is_fx_provider=1 の
参加銀行)が from_currency→to_currency の方向別レートを提示する。レートは
整数固定小数(rate = 1 単位 from に対する to の数量 × RATE_SCALE=1e8)。
ペアの bid/ask は「X→Y」「Y→X」の2行で表現し、スプレッドはrate(X→Y)·rate(Y→X) < 1e16。ルーティング(src/zc/fx/routing.ts)が ACTIVE な
見積を読み、最良実効レート/経路を選ぶ。
CREATE TABLE FxQuotes (
quote_id TEXT PRIMARY KEY, -- 'FXQ-<uuid>'
fxp_bank_id TEXT NOT NULL, -- FXP(参加銀行)
from_currency TEXT NOT NULL, -- ISO 4217(売り=入力)
to_currency TEXT NOT NULL, -- ISO 4217(買い=出力)
rate INTEGER NOT NULL, -- to/from × RATE_SCALE(1e8)
min_amount INTEGER NOT NULL DEFAULT 0, -- from 通貨建ての取引下限
max_amount INTEGER, -- from 通貨建ての取引上限(NULL=無制限)
valid_from TEXT NOT NULL, -- RFC3339
valid_to TEXT NOT NULL, -- RFC3339 見積有効期限
status TEXT NOT NULL DEFAULT 'ACTIVE', -- ACTIVE|WITHDRAWN
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_fxq_pair ON FxQuotes(from_currency, to_currency, status); -- ルーティングのペア検索
CREATE INDEX idx_fxq_fxp ON FxQuotes(fxp_bank_id, status); -- FXP単位の upsert/取下げ
FxTransfers(FX送金レコード)
クロスカレンシー FX 送金1件を表す(docs/specs/30_internal_design.md)。FX送金は FXP を導管とする
通貨別脚の GTID(payer→FXP は source 通貨、FXP→payee は target 通貨。ブリッジ時は
中間通貨の脚が増える)として既存 GTID レーンで原子決済される。本表は GtidTransactions
が持たない FX 固有の事実(採用経路・ロック見積・実効レート・脚を束ねる共有ハッシュ
ロック)を gtid 単位で保持する。
CREATE TABLE FxTransfers (
gtid TEXT PRIMARY KEY, -- 導管 GTID
from_currency TEXT NOT NULL,
to_currency TEXT NOT NULL,
amount_from INTEGER NOT NULL, -- payer 支払額(source)
amount_to INTEGER NOT NULL, -- payee 受取額(target)
effective_rate INTEGER NOT NULL, -- 合成 rate(from→to) × RATE_SCALE
hashlock TEXT NOT NULL, -- SHA-256 hex(共有HTLCハッシュロック)
route_json TEXT NOT NULL, -- FxRoute(hops/quotes/amounts)
quote_ids TEXT NOT NULL, -- 経路上の quote_id(カンマ連結)
status TEXT NOT NULL DEFAULT 'INITIATED', -- INITIATED|LOCKED|SETTLING|SETTLED|REFUNDED(`20_method_design.md` §17.4.5)
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
FxLegLocks(FX脚のHTLCロック)
クロスカレンシー FX のクロスレール原子性(docs/specs/30_internal_design.md §17.4)。各通貨脚
(導管のエッジ)を共有ハッシュロック+段階的タイムロック(上流ほど後に満了)で
ロックする。settle は claim(secret 公開)まで遅延し、claim で全脚を CLAIMED に
カスケードして初めて導管 GTID を登録・前進させる(全か無か)。未 claim なら
タイムロック満了で全脚 REFUNDED(資金移動なし)。決済の正は GTID 側。実装 src/zc/fx/htlc.ts。
CREATE TABLE FxLegLocks (
gtid TEXT NOT NULL, -- FxTransfers(gtid)
leg_index INTEGER NOT NULL, -- 0=payer→fxp0 … 末尾=fxp→payee
currency TEXT NOT NULL,
amount INTEGER NOT NULL,
from_bank_id TEXT NOT NULL,
from_account_hash TEXT NOT NULL,
to_bank_id TEXT NOT NULL,
to_account_hash TEXT NOT NULL,
hashlock TEXT NOT NULL, -- gtid内の全脚で共有
timelock TEXT NOT NULL, -- RFC3339(段階的:上流ほど後)
state TEXT NOT NULL DEFAULT 'LOCKED', -- LOCKED|CLAIMED|REFUNDED
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (gtid, leg_index),
FOREIGN KEY (gtid) REFERENCES FxTransfers(gtid)
);
CREATE INDEX idx_fxleglocks_state ON FxLegLocks(state, timelock); -- 期限切れLOCKEDの払戻スイープ
Cases(例外処理チケット)
CREATE TABLE Cases (
case_id TEXT PRIMARY KEY,
related_txid TEXT,
related_gtid TEXT,
state TEXT NOT NULL DEFAULT 'OPEN', -- CaseState: OPEN|IN_PROGRESS|RESOLVED|ESCALATED
reason_code TEXT NOT NULL,
description TEXT,
opened_by TEXT NOT NULL, -- 'ZC'|'BANK'|'OPS'
sla_deadline TEXT, -- 期限。超過で ESCALATED へ昇格(`20_method_design.md` §10.7.4)
evidence_refs TEXT, -- JSON array(証跡参照 ID の配列。`20_method_design.md` §10.10.2)
cause_key TEXT, -- 集約キー 'CAUSE:{cause_party_id|detection_path}:{reason_code}'(`20_method_design.md` §10.7.2)
cause_party_id TEXT, -- 原因主体識別子。検出時点で未特定なら NULL
detection_path TEXT, -- 原因主体が未特定のとき集約キーに用いる検出経路識別子
occurrence_count INTEGER NOT NULL DEFAULT 1, -- 束ねた取引の件数(集約のたびに増分)
last_occurred_at TEXT, -- 最終発生時刻(集約のたびに更新)
escalated_at TEXT, -- ESCALATED へ遷移した時刻。二次エスカレーション判定の基準
last_notified_at TEXT, -- 直近の二次通知時刻。同じ状況での再通知を抑止
resolved_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_case_txid ON Cases(related_txid);
CREATE INDEX idx_case_sla ON Cases(state, sla_deadline);
CREATE INDEX idx_case_cause ON Cases(cause_key, state);
cause_keyが NULL の CASE は集約しない:原因主体も検出経路も与えられない起票は、単独の CASE として立つ。
CAUSE::{reason_code}を鍵にすると、原因の判らない CASE が理由コードだけで束ねられ、「1 原因 1 件」ではなく「1 単語 1 件」になるためである。
occurrence_countは取引の数であって検出の数ではない:増分は
CaseRelatedTransactionsへの行挿入が成功した場合に限る(同一 txid の再検出では動かない)。これにより
20_method_design.md§10.7.2.2 の件数閾値が、再送量ではなく被害の広がりを表す。
CaseRelatedTransactions(集約 CASE に束ねた取引・INSERT ONLY)
集約された CASE の内訳。件数は Cases.occurrence_count が保持し、本表はどの取引が
束ねられたかを保持する(20_method_design.md §10.7.2「txid は関連付けで管理」)。
CREATE TABLE CaseRelatedTransactions (
case_id TEXT NOT NULL,
link_key TEXT NOT NULL, -- related_txid ?? related_gtid
related_txid TEXT,
related_gtid TEXT,
linked_at TEXT NOT NULL,
PRIMARY KEY (case_id, link_key),
FOREIGN KEY (case_id) REFERENCES Cases(case_id)
);
CREATE INDEX idx_case_rel_txid ON CaseRelatedTransactions(related_txid);
CREATE INDEX idx_case_rel_gtid ON CaseRelatedTransactions(related_gtid);
(case_id, link_key)の主キーが二重計上を構造的に防ぐ。挿入はINSERT OR IGNOREであり、件数の増分と
EntityStateLogへの追記はいずれもこの挿入が行を追加した場合(
changes() > 0)に限って実行される。同一 1 バッチであるから、「束ねたのに件数が動いていない」「件数だけ動いて内訳がない」のいずれも生じない。
sla_deadlineは必ず入る:openCaseは呼び出し側が指定しない場合、既定値(
CASE_SLA_SEC)から期限を計算して必ず埋める。期限の無い CASE は
Auto-Progress → Manual-Only の昇格判定(20_method_design.md§10.7.4)の対象から
落ちてしまい、「待ち続けたまま誰にも気づかれない」状態になるため、NULL 許容の列ではあるが実運用では空にしない。
AccessAuditLog(アクセス監査台帳・INSERT ONLY)
当事者スコープの照会について「誰が・何を・どの目的コードで読み、どう判定されたか」を残す
(10_requirements.md §3.3.2.2.1.1-1)。拒否も記録する——誰が断られたかは、設定ミスの
クライアントと探索行為を分ける唯一の手掛かりであり、10_requirements.md §3.3.2.2.1.1-3 の
事後監査はこの列が無ければ成立しない。認可の機構は src/zc/platform/access.ts、対象経路の一覧はsrc/zc/platform/access_routes.ts(規範は 32_api_contracts.md § 照会の認可)。
CREATE TABLE AccessAuditLog (
access_id TEXT PRIMARY KEY,
occurred_at TEXT NOT NULL, -- RFC3339
subject_type TEXT NOT NULL, -- AccessSubjectType: OPERATOR|PARTICIPANT|UNIDENTIFIED
subject_id TEXT, -- PARTICIPANT のとき bank_id、他は NULL
purpose_code TEXT, -- P01..P07。欠落時は NULL(=拒否の記録)
resource TEXT NOT NULL, -- 'transactions/TX-1' 等、読まれた対象
decision TEXT NOT NULL, -- AccessDecision: PERMIT|DENY
reason_code TEXT, -- 拒否理由。PERMIT では NULL
export_ref TEXT -- エクスポートを伴う読み取りの持出参照
);
CREATE INDEX idx_access_audit_time ON AccessAuditLog(occurred_at);
CREATE INDEX idx_access_audit_subject ON AccessAuditLog(subject_type, subject_id, occurred_at);
書込み失敗で照会を落とさない(規範):
recordAccessは例外を投げない。監査台帳の書込み失敗を理由に正当な照会を失敗させると、台帳が全照会の可用性依存になる——
10_requirements.md§8.2.1 の A-2(照会系は Decision 系より高い可用性)と正面から衝突する。失敗はサーバログに残し、照会は続行する。
Vault(短期秘匿ストア)
CREATE TABLE Vault (
vault_ref TEXT PRIMARY KEY,
txid TEXT,
data_type TEXT NOT NULL, -- VaultDataType: AML_EVAL|PII|RISK_HINT|HTLC_PREIMAGE
payload_json TEXT NOT NULL, -- 暗号化不要(モック)
expires_at TEXT NOT NULL, -- TTL
is_evicted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE INDEX idx_vault_expires ON Vault(expires_at, is_evicted);
PsprRegistry(PSPR登録)
CREATE TABLE PsprRegistry (
pspr_ref TEXT PRIMARY KEY,
payee_bank_id TEXT NOT NULL,
account_hash TEXT NOT NULL,
capability_state TEXT NOT NULL DEFAULT 'ACTIVE', -- ACTIVE|SUSPENDED|REVOKED
digest TEXT NOT NULL, -- 内容ハッシュ(改ざん検知)
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
revoked_at TEXT
);
RtpRequests(RTP請求)
状態は state 列に一本化されている。payer 側受信一覧も本テーブルを直接参照する
(専用の通知ストレージは持たない)。
CREATE TABLE RtpRequests (
rtp_id TEXT PRIMARY KEY,
payee_bank_id TEXT NOT NULL,
payer_bank_id TEXT NOT NULL,
amount_value INTEGER NOT NULL,
-- 唯一の状態列。CREATED|NOTIFIED|ACCEPTED|TX_CREATED|COMPLETED|DECLINED|EXPIRED|FAILED
state TEXT NOT NULL DEFAULT 'CREATED',
attempt_count INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
linked_txid TEXT,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
payee_name TEXT,
description TEXT,
edi_ref TEXT,
notified_at TEXT,
linked_txid_new TEXT,
payer_account_id TEXT,
response_type TEXT,
responded_at TEXT,
payee_account_hash TEXT
);
IdempotencyKeys(冪等キー管理:ZC側)
CREATE TABLE IdempotencyKeys (
key TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'PROCESSING', -- PROCESSING|DONE
response_body TEXT,
created_at TEXT NOT NULL,
updated_at TEXT,
request_hash TEXT -- sha256hex(JSON body)。本文比較に対応しない呼び出し元(リプレイ防止nonceキー等)は NULL
);
timeout sweep(毎分)が2種類の掃除を行う:
- PROCESSING 孤児(作成から15分超): 取得元リクエストが completeIdempotency 前に
死んだキー。放置するとクライアントの再送が永遠に{result:'PROCESSING'}を受け取る
ため削除する。同一キーでの再実行はTransactions.idempotency_key UNIQUEが二重TXを防ぐ。 - DONE キー(作成から24h超): 応答リプレイキャッシュの TTL 失効。
request_hash が非NULLで、再送ボディのハッシュと不一致の場合は409 IDEMPOTENCY_KEY_CONFLICT(保存済み応答は返さない)。詳細はdocs/specs/32_api_contracts.md § 冪等性(横断仕様)。