第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 側を信じる。


マイグレーション運用

鉄則

  1. 統合スキーマ (0001_consolidated_schema.sql) を直接編集する。
    本リポジトリはスキーマを単一の統合ファイルで保持する参照実装であり、
    スキーマ変更は新しい連番ファイルを切らず、この 1 ファイルを唯一の正と
    して直接書き換える。31_schema.md(本ファイル)も必ず同じ変更で更新し、
    両者を一致させる。
    • 列の追加・変更は対象 CREATE TABLE の定義に直接列を足す(別の
      後置 ALTER を積まない)。確定形の 1 ファイルを保つ。
    • 本番運用上の注意: D1 の wrangler d1 migrations apply は適用済み
      ファイルの再適用を行わないため、すでにデプロイ済みの実 DB に同手法を
      適用する場合は別途の前進的マイグレーション(または再構築)が要る。
      本参照実装は「新規 DB にこの 1 ファイルを適用して完全形を得る」前提。
  2. test/helpers/d1-mock.ts の SCHEMA_MIGRATIONS は
    0001_consolidated_schema.sql 単体を保つ。
    テスト/ローカルは統合
    スキーマを毎回新規適用するため、スキーマ変更は同ファイルへの編集だけで
    反映される(配列に新ファイルを足さない)。
  3. インデックスは追加したら 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 § 冪等性(横断仕様)。


PDFを作成

フォントは初回だけ読み込むため、1回目は時間がかかります。

用紙
組み方向
表紙
本文