第14巻 DBスキーマ定義(3) ― Watcher・Bank・レガシー連携・索引

本巻は docs/specs/31_schema.md のWatcherObservation〜Foreign Key戦略(FinalityAnchor/SystemMode/HTLCクロスチェーン拡張/Bank側テーブル/初期データ/Index Catalog/Legacy Adapterテーブル)を収める。前巻→第13巻の続き。


ZC テーブル(WatcherObservation)

WatcherObservation(外部レール確定イベントの観測記録)

ZC コアは外部レール
(オンチェーン/IGS/海外レール)を直接観測しない。Watcher
(KeyRegistry.owner_type='EXTERNAL_RAIL'|'ATTESTER' として登録)が観測した
確定イベントの署名付き表明のみを信頼し、P1 SettlementProofRef を生成する。

CREATE TABLE WatcherObservation (
  observation_id  TEXT    PRIMARY KEY,        -- 'WOBS-<uuid>'
  source          TEXT    NOT NULL,           -- レール/Watcher識別子。例: 'ONCHAIN:ETH', 'IGS_BOJ', 'CB_TOKEN:ECB:ETH'
  external_ref    TEXT    NOT NULL,           -- チェーン上txハッシュ/IGS確認ID/アテステーションID
  venue           TEXT    NOT NULL,           -- 'IGS_BOJ'|'ONCHAIN'|'ATTESTATION'|'CB_TOKEN'
  proof_type      TEXT    NOT NULL,           -- ProofType(a/bのいずれの証跡か)
  issuer_ref      TEXT    NOT NULL,           -- SettlementProofRef.issuer_bank_id に転記
  watcher_key_id  TEXT    NOT NULL,           -- KeyRegistry.key_id(観測したWatcher)
  signature       TEXT    NOT NULL,           -- base64
  nonce           TEXT    NOT NULL,
  occurred_at     TEXT    NOT NULL,           -- RFC3339(Watcher主張の観測時刻)
  proof_ref       TEXT    NOT NULL,           -- JSON SettlementProofRef
  created_at      TEXT    NOT NULL,
  confirmations   INTEGER                     -- オンチェーン観測の承認数(onchain_min_confirmationsとの確認深度ゲート)
);
-- 1イベント×1 Watcher鍵で一意。同一 Watcher の重複観測は dedup し、別 Watcher の
-- 観測は n-of-m クォーラムへの1票として追加記録される。
CREATE UNIQUE INDEX idx_watcher_observation_source_ref_key ON WatcherObservation(source, external_ref, watcher_key_id);
CREATE INDEX idx_watcher_observation_event ON WatcherObservation(source, external_ref);
CREATE INDEX idx_watcher_observation_key ON WatcherObservation(watcher_key_id);

記録フロー(src/shared/watcher.ts の recordWatcherObservation()):

  1. (source, external_ref, watcher_key_id) が既に記録済みなら、署名を再検証せず
    既存行を返す(deduped: true)。同一 Watcher の重複観測は 20_method_design.md §7.7.2 の冪等として
    吸収する。一方、別 Watcher が同一イベントを報告した場合は dedup せず、検証して
    1行追加する(n-of-m クォーラムへの1票)。
  2. 未記録の場合、buildWatcherObservationPayload() の正規ペイロード
    (source, external_ref, venue, proof_type, issuer_ref)への
    Watcher の署名を KeyRegistry(§K)で検証する(KEY_* /
    EXTERNAL_SIGNATURE_INVALID / SIGNATURE_REPLAYED)。
  3. 検証済み鍵の owner_type が EXTERNAL_RAIL または ATTESTER であること
    (WATCHER_UNAUTHORIZED。それ以外の主体は Watcher として認めない)。
  4. WatcherObservation 行を保存し、venue/external_ref/signer_key_id/
    verified_at を備えた SettlementProofRef を返す。

Watcher クォーラム(信頼最小化): countDistinctWatchers() は、あるイベント
(source,external_ref)を観測した相異なる Watcher 運用主体(KeyRegistry.owner_ref。
key_id ではなく owner_ref で数えるため、1主体が複数鍵を持っても1票)の数を返す。
クロスチェーン HTLC の決済(recordOnchainFulfillment)は、HTLC の
onchain_min_watchers(既定 1)に達するまで HTLC_ONCHAIN_PENDING に留める。
これにより、単一の Watcher 鍵だけでクロスチェーン脚を確定できないようにする
(クォーラム未達の間は OnchainQuorumPending を FinalityLog に記録し、
reason_code='ONCHAIN_QUORUM_PENDING' とする)。


ZC テーブル(FinalityAnchor / FinalityCosign)

FinalityAnchor(透明性アンカリング)

検証可能性の強化(単一書き込み者・完全性)。ZC は単一書き手のままだが、
FinalityLog の各チェーン(finality_chain.ts)の先端ハッシュを
event_seq のハイウォーターマークで定期的にスナップショットし、
追記専用の FinalityAnchor 行へ固定する。これが「ZC が書き換えられない
参照点」となり、参加者は後から自チェーンの包含を独立検証できる。

CREATE TABLE FinalityAnchor (
  anchor_id          TEXT    PRIMARY KEY,        -- 'ANCHOR-<uuid>'
  anchor_seq         INTEGER NOT NULL,           -- アンカーの連番
  high_watermark_seq INTEGER NOT NULL,           -- アンカー時点の MAX(FinalityLog.event_seq)
  chain_tips_json    TEXT    NOT NULL,           -- JSON配列 {chain_id, tip_hash}(chain_id昇順)
  root_hash          TEXT    NOT NULL,           -- chain_tips_json の sha256
  created_at         TEXT    NOT NULL
);
CREATE UNIQUE INDEX idx_finality_anchor_seq ON FinalityAnchor(anchor_seq);
CREATE INDEX idx_finality_anchor_watermark ON FinalityAnchor(high_watermark_seq);

フロー(src/zc/finality/finality_anchor.ts):

  • createFinalityAnchor():全チェーンID(finality_audit.ts の
    listChainIds())について high_watermark_seq 時点の tip hash を
    getChainTipHashAsOf() で取得し、chain_tips_json/root_hash とともに
    新しい FinalityAnchor 行を追記する。FinalityLog が空の場合は null。
  • verifyChainInclusion(db, anchor_id, chain_id):アンカー時点の
    chain_id の tip hash を再計算し、アンカーに記録された値と一致するかを
    返す(included: boolean)。不一致はアンカー後にそのチェーンの
    entry_hash が書き換えられたことを示す。ANCHOR_NOT_FOUND /
    CHAIN_NOT_ANCHORED を reason_code として登録。
  • 配布先(全参加行配信/公開トランスペアレンシーログ/公開チェーンへの
    従的アンカリング)は運用上の決定であり、本実装は FinalityAnchor
    テーブルへの記録までを担う(§G オープン論点)。

FinalityCosign(参加行の副署)

自行が当事者となる TX / GTID / DNS チェーンについて、その現在の tip hash に
参加行が署名する(当事者判定: TX=payer/payee、GTID=leg 銀行、DNS=ネット
ポジション銀行)。KeyRegistry(owner_type='PARTICIPANT'、§K)で検証する。
chain_kind 列が対象チェーン種別を記録する。

CREATE TABLE FinalityCosign (
  cosign_id      TEXT    PRIMARY KEY,        -- 'COSIGN-<uuid>'
  chain_id       TEXT    NOT NULL,           -- FinalityLog chain id(txid/gtid/cycle_id)
  participant_id TEXT    NOT NULL,           -- 副署する参加行の bank_id
  entry_hash     TEXT    NOT NULL,           -- 副署対象の tip entry_hash
  signer_key_id  TEXT    NOT NULL,           -- KeyRegistry.key_id(owner_type='PARTICIPANT', owner_ref=participant_id)
  signature      TEXT    NOT NULL,           -- base64
  nonce          TEXT    NOT NULL,
  occurred_at    TEXT    NOT NULL,           -- RFC3339(参加行主張)
  created_at     TEXT    NOT NULL,
  chain_kind     TEXT                        -- TX|GTID|DNS
);
CREATE UNIQUE INDEX idx_finality_cosign_chain_participant_entry ON FinalityCosign(chain_id, participant_id, entry_hash);
CREATE INDEX idx_finality_cosign_participant ON FinalityCosign(participant_id);

CosignPolicy(副署の必須化ポリシー)

チェーン種別ごとに副署を必須化する設定。is_mandatory=1 のチェーン種別は、
現在の tip に対して min_cosigners 以上の異なる参加行が副署して初めて
「外部検証済み」とみなせる(checkCosignRequirement() が判定)。

CREATE TABLE CosignPolicy (
  chain_kind     TEXT PRIMARY KEY,            -- TX|GTID|DNS
  min_cosigners  INTEGER NOT NULL DEFAULT 1,
  is_mandatory   INTEGER NOT NULL DEFAULT 0,
  updated_at     TEXT NOT NULL
);

フロー(recordFinalityCosign()):

  1. chain_id が co-signable(TX-* / GT-*・GTID-* / DNS-*)であり、
    participant_id が当該チェーンの当事者であること
    (COSIGN_NOT_APPLICABLE。GLOBAL チェーン・非当事者は対象外)。
  2. チェーンに最低1件の FinalityLog エントリがあること
    (COSIGN_ENTRY_NOT_FOUND)。
  3. {chain_id, entry_hash} への参加行の署名を KeyRegistry(§K)で検証
    (KEY_* / EXTERNAL_SIGNATURE_INVALID / SIGNATURE_REPLAYED)。
  4. 検証済み鍵が owner_type='PARTICIPANT' かつ owner_ref=participant_id
    であること(COSIGN_PARTICIPANT_MISMATCH)。
  5. (chain_id, participant_id, entry_hash) が既存なら署名再検証せず既存行
    を返す(同一 tip への再副署は冪等)。

副署の必須/任意(§G オープン論点):本実装は副署を任意の追加証跡
として扱い、決済フローの状態機械(受理・決済判定)には影響しない。


ZC テーブル(SystemMode)

SystemMode(ZC全体のBCP縮退モード)

DnsCycles.HOLD_ACTIVE
(DNS_HOLD)と同じ「不確定時は Read-only へ縮退」原則を、ベンダー障害
(Cloudflare 障害等)シナリオまで拡張する。id=1 の単一行テーブルで
ZC 全体の運用モードを保持する。

CREATE TABLE SystemMode (
  id           INTEGER PRIMARY KEY CHECK (id = 1),
  mode         TEXT NOT NULL DEFAULT 'NORMAL',  -- NORMAL | BCP_READONLY
  reason       TEXT,
  activated_at TEXT,
  updated_at   TEXT NOT NULL
);

フロー(src/zc/platform/system_mode.ts):

  • getSystemMode():現在のモードを返す(seed行が無い場合は NORMAL を既定値とする)。
  • assertNotBcpReadOnly(mode):mode === 'BCP_READONLY' のとき
    SYSTEM_BCP_READ_ONLY(reason_code、docs/specs/32_api_contracts.md §エラーカタログ)
    を投げる。新規の資金移動を開始する ingress ハンドラ向け。照会系
    (ステータス参照など)は対象外。
  • activateBcpReadOnly(env, reason) / deactivateBcpReadOnly(env):
    NORMAL ⇄ BCP_READONLY の遷移。冪等(既に目的のモードであれば
    FinalityLog への二重書き込みを行わず既存行を返す)。遷移を
    FinalityLog の GLOBAL チェーン(txid IS NULL AND gtid IS NULL)に
    SystemBcpActivated / SystemBcpDeactivated として記録する。

API:GET /api/system-mode(現在のモード取得)、
POST /internal/system-mode/bcp-activate({reason} を指定して
BCP_READONLY へ移行)、POST /internal/system-mode/bcp-deactivate
(NORMAL へ復帰)。

可搬性(Cloudflare固有バインディングの抽象化層)は本実装の対象外
(§H オープン論点として残る)。


HtlcContracts クロスチェーン拡張

クロスチェーンHTLC(HtlcContracts の cross_chain_* / onchain_* 列)

オンチェーン決済手段との接続(クロスチェーンHTLC)。列定義は上記 HtlcContracts
を参照(cross_chain_source / onchain_timelock / onchain_lock_ref /
onchain_lock_proof_json / onchain_release_proof_json)。
cross_chain_source が NULL の場合は通常の ZC 側 HTLC と完全に同一の
動作。cross_chain_source が設定されている場合、
同じ hashlock が source(例: 'ONCHAIN:ETH')下のオンチェーンエスクロー
もロックしており、ZC はチェーンを直接検査せず Watcher(20_method_design.md §7.7)の
署名付き観測(SettlementProofRef、venue=ONCHAIN)のみを受理する。

不変条件:

  • onchain_timelock(オンチェーン側の内側タイムロック)は必ず timelock
    (ZC側の外側タイムロック)より厳密に前(createHtlc が
    ONCHAIN_TIMELOCK_INVALID で拒否)。ZC側のH予約は常にオンチェーン側より
    長く保持される。
  • 同一 hashlock(= secret_hash)が ZC 側・オンチェーン側の両レッグを
    アンロックする。二重決済は不可(HTLC_FULFILL_REQUESTED への CAS が
    単一のソース状態からのみ許可される)。

状態遷移(src/zc/lanes/htlc.ts):

  • HTLC_LOCKED → HTLC_ONCHAIN_PENDING(recordCrossChainLock、
    CrossChainLocked イベント。Watcherがオンチェーンエスクローのロックを
    観測)。onchain_lock_ref / onchain_lock_proof_json を記録。
  • HTLC_ONCHAIN_PENDING → HTLC_FULFILL_REQUESTED → DECIDED_TO_SETTLE
    → ...(recordOnchainFulfillment、OnchainProofObserved イベント。
    Watcherがオンチェーンエスクローのプリイメージ公開を観測。
    settleAfterPreimage で claimHtlc と同じ決済シーケンスを共有)。
    onchain_release_proof_json を記録。
  • プリイメージが hashlock に一致しない場合: ONCHAIN_PROOF_MISMATCH
    (Watcherの署名/nonceを消費する前に拒否、HtlcClaimRejected ログ)。
  • onchain_timelock 超過かつ未フルフィルの場合: ONCHAIN_TIMEOUT で
    cancelHtlc(HTLC_ONCHAIN_PENDING も cancelHtlc の対象状態に含まれる)。
    ZC側 timelock 超過の場合は既存の TIMELOCK_EXPIRED で cancelHtlc。
    どちらもタイムアウトスイープ(src/cron/timeout_sweep.ts)が定期的に検出。

API: POST /api/htlc/:htlc_id/cross-chain-lock /
POST /api/htlc/:htlc_id/onchain-fulfillment(いずれも Watcher 専用、
docs/specs/32_api_contracts.md 参照)。


Bank基本テーブル

BankAccounts(口座マスター)

CREATE TABLE BankAccounts (
  account_id    TEXT PRIMARY KEY,              -- UUID
  bank_id       TEXT NOT NULL,                 -- '001'|'002'
  customer_id   TEXT NOT NULL,
  customer_name TEXT NOT NULL,                 -- 名義(名義確認用)
  account_type  TEXT NOT NULL DEFAULT 'SAVINGS', -- SAVINGS|CURRENT|SUSPENSE|SETTLEMENT|ASSET|BOJ
  status        TEXT NOT NULL DEFAULT 'NORMAL',  -- NORMAL|FROZEN|CLOSING_HOLD|CLOSED
  freeze_reason TEXT,
  opened_at     TEXT NOT NULL,
  closed_at     TEXT
);
CREATE INDEX idx_acct_bank     ON BankAccounts(bank_id, status);
CREATE INDEX idx_acct_customer ON BankAccounts(customer_id);

BankJournals(元帳:ゼロサム・INSERT ONLY)

CREATE TABLE BankJournals (
  journal_id  TEXT    PRIMARY KEY,             -- UUID
  bank_id     TEXT    NOT NULL,
  account_id  TEXT    NOT NULL,
  amount      INTEGER NOT NULL,                -- 符号付き(正=増加、負=減少)
  amount_currency TEXT NOT NULL DEFAULT 'JPY', -- ISO 4217。元帳金額の通貨次元
  tx_type     TEXT    NOT NULL,                -- TRANSFER|RESERVE|EXECUTE|CREDIT|INTEREST|CASH|CORRECTION
  txid        TEXT,                            -- ZC取引ID(外部参照)
  tx_group_id TEXT    NOT NULL,                -- 仕訳グループ(ゼロサム確認単位)
  description TEXT,
  value_date  TEXT    NOT NULL,                -- 勘定日付 'YYYY-MM-DD'
  created_at  TEXT    NOT NULL
);
CREATE INDEX idx_jnl_account ON BankJournals(account_id, value_date);
CREATE INDEX idx_jnl_txid    ON BankJournals(txid);
CREATE INDEX idx_jnl_group   ON BankJournals(tx_group_id);
CREATE INDEX idx_jnl_account_ccy ON BankJournals(account_id, amount_currency);

通貨次元(amount_currency): 共有の透明・別段勘定(suspense {bank}0000000、

ZC清算 {bank}-ZCS、利益剰余 {bank}-RE)は通貨で口座分離されないため、

単純な SUM(amount) は非JPYフローが混じると単位を取り違える。各行に通貨を

持たせ、ゼロサム検証(verifyZeroSum)と残高計算(calcBalance(account_id, ccy))を

通貨ごとに行う。insertJournalGroup のゼロサム検査も通貨別(PvP のような

多通貨グループは各通貨が独立に均衡することを要求)。既存行は DEFAULT 'JPY' で

後方互換。

ZcRequests(ZC指示の冪等管理)

CREATE TABLE ZcRequests (
  request_id    TEXT PRIMARY KEY,              -- ZCのidempotency_key
  bank_id       TEXT NOT NULL,
  txid          TEXT,
  command_type  TEXT NOT NULL,                 -- reserve-funds|execute-debit|execute-credit|...
  status        TEXT NOT NULL DEFAULT 'PROCESSING', -- PROCESSING|DONE|PROOF_ISSUED
  response_body TEXT,                          -- 処理済みレスポンスJSON(重複時に返す)
  created_at    TEXT NOT NULL,
  updated_at    TEXT
);
CREATE INDEX idx_zcreq_txid ON ZcRequests(txid);

SuspenseDetails(別段預金明細)

CREATE TABLE SuspenseDetails (
  suspense_id    TEXT    PRIMARY KEY,          -- UUID
  bank_id        TEXT    NOT NULL,
  account_id     TEXT    NOT NULL,             -- 元口座
  direction      TEXT    NOT NULL,             -- PAY|RECEIVE|HV_TRANSIT|HTLC
  status         TEXT    NOT NULL,             -- SuspenseStatus
  amount         INTEGER NOT NULL,
  txid           TEXT,
  request_id     TEXT,                         -- ZC request_id
  dns_cycle_id   TEXT,
  expires_at     TEXT,                         -- HTLC timelock
  custody_reason TEXT,                         -- CUSTODY時の理由
  settled_at     TEXT,
  created_at     TEXT    NOT NULL,
  updated_at     TEXT    NOT NULL
);
CREATE INDEX idx_susp_account ON SuspenseDetails(account_id, status);
CREATE INDEX idx_susp_txid    ON SuspenseDetails(txid);

CUSTODY(凍結/解約/該当なし口座宛の受取資金)の解消経路は2つ:

  1. 手動: teller API POST /bank/:bankId/v1/teller/suspense/:suspenseId/resolve(SETTLE/RETURN)
  2. 自動: timeout sweep(毎分)の releaseRecoveredCustody が、口座が
    NORMAL+SAVINGS に復旧した CUSTODY レコードを顧客口座へ自動入金(CUSTODY→SETTLED)。
    NOT_FOUND / SYSTEM_ACCOUNT 由来(account_id が別段口座を指す)は対象外で、手動解消のみ。

DailyBalances(日次残高スナップショット)

CREATE TABLE DailyBalances (
  account_id       TEXT    NOT NULL,
  snapshot_date    TEXT    NOT NULL,             -- 'YYYY-MM-DD'
  end_of_day_balance INTEGER NOT NULL,
  PRIMARY KEY (account_id, snapshot_date)
);

InterestRates(利率マスター)

CREATE TABLE InterestRates (
  rate_id        TEXT PRIMARY KEY,
  bank_id        TEXT NOT NULL,
  account_type   TEXT NOT NULL,
  annual_rate    REAL NOT NULL,                -- 例: 0.001 = 0.1%
  effective_from TEXT NOT NULL,
  effective_to   TEXT
);

Bank テーブル(監査ログ・着金フィルタ)

BankAuditLog(Bank側 コマンド監査ログ:INSERT ONLY)

CREATE TABLE BankAuditLog (
  log_id       TEXT    PRIMARY KEY,              -- UUID
  bank_id      TEXT    NOT NULL,
  txid         TEXT,
  request_id   TEXT,                             -- ZC request_id
  command      TEXT    NOT NULL,                 -- reserve-funds|execute-debit|...
  status       TEXT    NOT NULL,                 -- 'OK'|'NG'
  reason_code  TEXT,
  amount       INTEGER,
  account_id   TEXT,
  details_json TEXT,
  occurred_at  TEXT    NOT NULL
);
CREATE INDEX idx_audlog_bank ON BankAuditLog(bank_id, occurred_at);
CREATE INDEX idx_audlog_txid ON BankAuditLog(txid);
CREATE INDEX idx_audlog_req  ON BankAuditLog(request_id);

PaymentFilters(着金フィルタリングルール)

CREATE TABLE PaymentFilters (
  filter_id      TEXT    PRIMARY KEY,
  bank_id        TEXT    NOT NULL,
  scope          TEXT    NOT NULL DEFAULT 'ACCOUNT',  -- 'BANK_WIDE'|'ACCOUNT'
  account_id     TEXT,                           -- scope=ACCOUNT の場合の対象口座
  filter_type    TEXT    NOT NULL,
  -- 'SENDER_BLOCK'      : 特定送金元口座ハッシュをブロック
  -- 'SENDER_BANK_BLOCK' : 特定送金元銀行IDをブロック
  -- 'AMOUNT_LIMIT'      : 金額上限(超過は action 適用)
  -- 'EDI_PATTERN'       : 電文EDIのパターンマッチ
  -- 'REQUIRE_APPROVAL'  : 全着金に顧客承認を要求
  condition_json TEXT    NOT NULL,
  action         TEXT    NOT NULL,               -- 'REJECT'|'HOLD_CONFIRM'|'HOLD_MANUAL'
  description    TEXT,
  is_active      INTEGER NOT NULL DEFAULT 1,
  created_by     TEXT    NOT NULL,
  created_at     TEXT    NOT NULL,
  updated_at     TEXT    NOT NULL
);
CREATE INDEX idx_filter_bank    ON PaymentFilters(bank_id, is_active);
CREATE INDEX idx_filter_account ON PaymentFilters(account_id, is_active);

PaymentApprovalRequests(着金承認待ちリクエスト)

CREATE TABLE PaymentApprovalRequests (
  approval_id         TEXT    PRIMARY KEY,
  bank_id             TEXT    NOT NULL,
  account_id          TEXT    NOT NULL,
  txid                TEXT    NOT NULL,
  filter_id           TEXT    NOT NULL,
  status              TEXT    NOT NULL DEFAULT 'PENDING', -- PENDING|APPROVED|REJECTED|TIMEOUT
  sender_bank_id      TEXT    NOT NULL,
  sender_account_hash TEXT,
  amount_value        INTEGER NOT NULL,
  edi_data            TEXT,
  expires_at          TEXT    NOT NULL,
  responded_at        TEXT,
  created_at          TEXT    NOT NULL,
  updated_at          TEXT    NOT NULL
);
CREATE INDEX idx_approval_account ON PaymentApprovalRequests(account_id, status);
CREATE INDEX idx_approval_txid    ON PaymentApprovalRequests(txid);

初期データ

ZC側

INSERT OR IGNORE INTO Participants (...) VALUES
  ('001', 'みずほ銀行',   '/bank/001', 100000000, 0, 1, '2025-01-01T00:00:00Z'),
  ('002', '三菱UFJ銀行', '/bank/002', 100000000, 0, 1, '2025-01-01T00:00:00Z');

Bank側

口座命名規則: {bankId}0000000=別段預金, {bankId}-ZCS=清算勘定, {bankId}-CASH=現金, {bankId}-BOJ=日銀預け金

-- 口座マスター(システム勘定 + 顧客口座)
-- 001行: 別段預金, ZC清算勘定, 現金, 日銀預け金, 顧客2名
-- 002行: 同上
-- 各顧客口座の初期残高: 100万円(ゼロサム仕訳でZC清算勘定と相殺)
-- 利率: 普通預金 0.1%(001・002共通)

Index Catalog

D1 はクエリプランナの統計情報が貧弱で、行数が増えるとインデックス無し
クエリの p99 が急速に悪化する。下表は現在実装されているクエリが
利用するインデックスを網羅したもの。新規クエリ追加時は本表をまず確認し、
既存インデックスで賄えない場合は統合スキーマ 0001_consolidated_schema.sql
に索引を直接追加し、本カタログにも記載すること。なお本ファイル上部の各テーブル CREATE TABLE スニペットは
代表的なインデックスのみを併記する場合があり、索引の網羅的な正(authoritative)は
本カタログ(および migrations/0001_consolidated_schema.sql)とする。

Transactions

Index Columns Backed query
idx_tx_state (state) 状態別一覧
idx_tx_owner (owner, state) timeout sweep の owner='ZC' 述語(単一所有者則)
idx_tx_payer (payer_bank_id, state) 銀行毎の出金照会
idx_tx_payee (payee_bank_id, state) 銀行毎の入金照会
idx_tx_dns (dns_cycle_id) DNS 清算明細生成
idx_tx_updated_at (updated_at) 更新時刻順の走査(timeout sweep の期限判定は pending_since 側。上記 Transactions の列注記)
idx_tx_lane_state (lane, state) レーン × 状態のダッシュボードフィルタ
idx_transactions_mandate_id (mandate_id) mandate別の関連TX照会(委任チェーン)

FinalityLog

Index Columns Backed query
idx_fl_txid (txid) TX 単体トレース
idx_fl_gtid (gtid) GTID 単体トレース
idx_fl_seq (event_seq) 全体時系列
idx_fl_chain_seq (txid, event_seq) ハッシュチェーン検証(TX)
idx_fl_gchain_seq (gtid, event_seq) ハッシュチェーン検証(GTID)
idx_fl_occurred_at (occurred_at) 時間範囲監査(GET /api/events?limit=&offset=)
idx_fl_chain_prev_hash (txid, prev_hash) WHERE … TX チェーン分岐防止(部分 UNIQUE)
idx_fl_event_seq_unique (event_seq) event_seq 重複防止(UNIQUE)
idx_fl_gtid_chain_prev_hash (gtid, prev_hash) WHERE … GTID 専用チェーン分岐防止(部分 UNIQUE)

HtlcContracts

Index Columns Backed query
idx_htlc_payee_state (payee_bank_id, state) timeout sweep / payee 側 HTLC 一覧
idx_htlc_payer_state (payer_bank_id, state) payer 側 HTLC 一覧
idx_htlc_timelock (timelock, state) timelock 期限切れ抽出
idx_htlc_condition_template (condition_template_id) プログラマビリティ: condition_template_id別のHTLC照会

RtpRequests

Index Columns Backed query
idx_rtp_payer_state (payer_bank_id, state) 銀行毎の RTP 一覧
idx_rtp_payee_state (payee_bank_id, state) 銀行毎の RTP 一覧
idx_rtp_expires (expires_at, state) 期限切れ RTP 巡回

Cases / IdempotencyKeys / DnsCycles

Index Columns Backed query
idx_case_txid (related_txid) TX に紐づくケース照会
idx_case_sla (state, sla_deadline) 期限超過 CASE の昇格スイープ(20_method_design.md §10.7.4)
idx_case_state (state, created_at) OPEN/IN_PROGRESS のケース一覧
idx_case_gtid (related_gtid) GTID に紐づくケース照会
idx_case_cause (cause_key, state) 集約先の探索「この原因の未解決 CASE は既にあるか」(20_method_design.md §10.7.2)
idx_case_rel_txid (related_txid) 集約 CASE の内訳照会(txid 側)
idx_case_rel_gtid (related_gtid) 集約 CASE の内訳照会(gtid 側)
idx_idemp_created (created_at) timeout sweep の冪等キー掃除(PROCESSING孤児 15分 / DONE 24h TTL)
idx_dns_state (state, created_at) DNS サイクル状態別一覧
idx_dns_business_date (business_date) business_date によるサイクル照会(非UNIQUE)

GtidLegs

Index Columns Backed query
idx_legs_gtid (gtid) GTID単位でのレグ一覧取得
idx_legs_txid (txid) orchestrator.ts: onPayeeExecConfirmed / suspendTx の txid 逆引き

AccessAuditLog

Index Columns Backed query
idx_access_audit_time (occurred_at) 期間指定の監査レビュー
idx_access_audit_subject (subject_type, subject_id, occurred_at) 主体別のアクセス履歴(横断突合の事後検知。10_requirements.md §3.3.2.2.1.1-3)

KeyRegistry

Index Columns Backed query
idx_key_registry_owner (owner_type, owner_ref) 主体別の鍵一覧(鍵ローテーション・失効操作)

Attestation

Index Columns Backed query
idx_attestation_subject (subject_ref) 取引/レグ単位のアテステーション一覧
idx_attestation_template (template_id) テンプレート単位の利用状況集計

Mandate

Index Columns Backed query
idx_mandate_principal (principal_participant_id) principal別のmandate一覧(失効操作)
idx_mandate_grantee (grantee_ref) grantee別のmandate一覧
idx_mandate_parent (parent_mandate_id) 委任チェーンの子mandate探索

DebitMandate

Index Columns Backed query
idx_ddm_payer (payer_bank_id, payer_account_alias, state) 顧客の「私が許可している引き落とし一覧」
idx_ddm_payee (payee_bank_id, payee_account_hash, state) 受取人別の契約一覧・資格喪失時の一括失効
idx_ddm_mandate (mandate_id) Mandate 失効時に波及する継続収納契約の探索

ScheduledCollection

Index Columns Backed query
uq_collection_charge_ok (dd_mandate_id, charge_ref) WHERE result='CONFIRMED_OK' 二重収納の防止とラダー排他(OCO)。部分ユニーク
idx_collection_due (due_date, state) 振替日の発火対象の抽出・充当順序の算定
idx_collection_ddm (dd_mandate_id, charge_ref) 契約単位の照会(予定を含む)・ラダーの段の探索
idx_collection_txid (txid) 実行中の Transactions から予告への逆引き
idx_collection_freeze (amend_freeze_at, state) 凍結時刻到来分の掃引(cron)

CollectionAttempt

Index Columns Backed query
idx_cattempt_collection (collection_id, attempt_no) 試行系列の時系列取得(現在状態の導出)

WatcherObservation

Index Columns Backed query
idx_watcher_observation_source_ref_key (source, external_ref, watcher_key_id) UNIQUE 1イベント×1 Watcher鍵で一意(同一Watcherは dedup、別Watcherは追加票)
idx_watcher_observation_event (source, external_ref) イベント単位の観測一覧(クォーラム集計)
idx_watcher_observation_key (watcher_key_id) Watcher別の観測一覧(鍵失効時の影響調査)

FinalityAnchor / FinalityCosign

Index Columns Backed query
idx_finality_anchor_seq (anchor_seq) UNIQUE アンカーの連番引き
idx_finality_anchor_watermark (high_watermark_seq) ウォーターマークによるアンカー検索
idx_finality_cosign_chain_participant_entry (chain_id, participant_id, entry_hash) UNIQUE 副署の冪等チェック
idx_finality_cosign_participant (participant_id) 参加行別の副署一覧

Legacy Adapter(対外接続系)

Index Columns Backed query
idx_outbox_pending (bank_id, status) 未適用(PENDING)postingのドレイン走査(drainOutbox)
idx_outbox_claimed (status, claimed_at) CLAIMED のまま放置された行の失効回収(recoverStaleClaims)
idx_notify_unread (bank_id, status) 未読(UNREAD)通知のプル取得(pullNotifications)

その他の表(Vault、Bank: BankAccounts, BankJournals, …)
は定義済みの既存インデックスで賄える。


Legacy Adapter テーブル(対外接続系)

現実の勘定系に見られる制約(バッチ窓・非冪等・予約プリミティブ無し・
ミッドバッチ照会不可・タイムアウト・push口無し)を意図的に再現した
敵対的コアモデルを前提とする。この前提の上で、ZC からはクリーンな
24/365・冪等・リアルタイムな面に見せるためのアダプタ層である。設計とテストは
30_internal_design.md(レガシー勘定系アダプタ 内部設計)、実装は src/bank/legacy/、
適合性検証テストは test/bank/legacy/adversarial.test.ts。

テーブル 役割 対応する提案
LegacyProfiles 参加者ごとの能力プロファイル(role / reservation_mode / settlement_mode / notify_mode / window 等)。異機種を分岐ではなく設定で飲む。 #1
LegacyCoreAccounts 敵対的コアモデルの権威残高(balance と customer_name のみ。予約カラム無し。複式簿記ではない — 現実のコアは通常内部で複式簿記を保つため、この単純化は既知の割り切り)。customer_name は name-check(7)/account-verify(8) の裏付け。 —
LegacyCoreJournal コアが実際に適用した posting の追記ログ。txid を持ち監査追跡可能(request_id はアダプタ内部の相関idに過ぎない)。二重適用の検知に使う。 —
AdapterShadow 利用可能残高ミラー(available / reserved)。コアに触れず承認。reserved はコアが持てない予約を吸収。 #2
AdapterOutbox store-and-forward。shadow で承認済みだがコア未適用の posting。txid を保持。status は PENDING → CLAIMED → APPLIED(または BLOCKED)の3〜4状態遷移で、claim-then-apply により同時 drain 呼出し下でも二重適用しない。 #2 / #4 / #6
AdapterNotifications プル型通知ストア。コアに push 口を要求しない。 #5
AdapterReconDrift 三者照合(core vs shadow vs outbox)のドリフト記録。case_id で実際の Cases 行に紐づき、status=OPEN は本物の CASE として運用監視される。 #3

以下は該当7テーブルの確定形DDL(migrations/0001_consolidated_schema.sql からの転記)。
索引(idx_outbox_pending / idx_outbox_claimed / idx_notify_unread)は上記 Index Catalog にも記載済み。

CREATE TABLE LegacyProfiles (
  bank_id             TEXT PRIMARY KEY,
  role                TEXT    NOT NULL DEFAULT 'FULL',      -- FULL | PAYEE_ONLY | PAYER_ONLY
  reservation_mode    TEXT    NOT NULL DEFAULT 'SUSPENSE',  -- SUSPENSE | NONE
  settlement_mode     TEXT    NOT NULL DEFAULT 'DIRECT',    -- DIRECT | PREFUNDED_SHADOW
  notify_mode         TEXT    NOT NULL DEFAULT 'PUSH',      -- PUSH | PULL
  sync_reserve        INTEGER NOT NULL DEFAULT 1,           -- can the core hold/return a reservation synchronously
  realtime_name_check INTEGER NOT NULL DEFAULT 1,           -- can the core answer name-check in real time
  batch_ingest        INTEGER NOT NULL DEFAULT 0,           -- prefers file/bulk ingest over N synchronous calls
  window_open_hour    INTEGER,                              -- JST hour the core comes online (NULL = always online)
  window_close_hour   INTEGER,                              -- JST hour the core goes offline for batch
  created_at          TEXT    NOT NULL
);

CREATE TABLE LegacyCoreAccounts (
  bank_id       TEXT    NOT NULL,
  account_id    TEXT    NOT NULL,
  balance       INTEGER NOT NULL DEFAULT 0,
  customer_name TEXT,
  PRIMARY KEY (bank_id, account_id)
);

CREATE TABLE LegacyCoreJournal (
  seq        INTEGER PRIMARY KEY AUTOINCREMENT,
  bank_id    TEXT    NOT NULL,
  account_id TEXT    NOT NULL,
  amount     INTEGER NOT NULL,   -- signed: DEBIT negative, CREDIT positive
  op         TEXT    NOT NULL,   -- DEBIT | CREDIT
  txid       TEXT,
  request_id TEXT,
  applied_at TEXT    NOT NULL
);

CREATE TABLE AdapterShadow (
  bank_id    TEXT    NOT NULL,
  account_id TEXT    NOT NULL,
  available  INTEGER NOT NULL DEFAULT 0,
  reserved   INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (bank_id, account_id)
);

CREATE TABLE AdapterOutbox (
  outbox_id  TEXT    PRIMARY KEY,
  bank_id    TEXT    NOT NULL,
  account_id TEXT    NOT NULL,
  op         TEXT    NOT NULL,                   -- DEBIT | CREDIT
  amount     INTEGER NOT NULL,                   -- unsigned magnitude
  txid       TEXT,
  request_id TEXT    NOT NULL,
  status     TEXT    NOT NULL DEFAULT 'PENDING', -- PENDING | CLAIMED | APPLIED | BLOCKED
  attempts   INTEGER NOT NULL DEFAULT 0,
  claimed_at TEXT,
  created_at TEXT    NOT NULL,
  applied_at TEXT
);
CREATE INDEX idx_outbox_pending ON AdapterOutbox (bank_id, status);
CREATE INDEX idx_outbox_claimed ON AdapterOutbox (status, claimed_at);

CREATE TABLE AdapterNotifications (
  notify_id  TEXT    PRIMARY KEY,
  bank_id    TEXT    NOT NULL,
  txid       TEXT    NOT NULL,
  account_id TEXT,
  amount     INTEGER NOT NULL,
  status     TEXT    NOT NULL DEFAULT 'UNREAD', -- UNREAD | READ
  created_at TEXT    NOT NULL,
  read_at    TEXT
);
CREATE INDEX idx_notify_unread ON AdapterNotifications (bank_id, status);

CREATE TABLE AdapterReconDrift (
  drift_id        TEXT    PRIMARY KEY,
  bank_id         TEXT    NOT NULL,
  account_id      TEXT    NOT NULL,
  core_balance    INTEGER NOT NULL,
  shadow_expected INTEGER NOT NULL,
  drift_amount    INTEGER NOT NULL,
  case_id         TEXT,
  status          TEXT    NOT NULL DEFAULT 'OPEN', -- OPEN | RESOLVED
  detected_at     TEXT    NOT NULL
);

冪等性は専用テーブルを持たず、既存の IdempotencyKeys(src/shared/idempotency.ts
の resolveIdempotency/completeIdempotency)を再利用する。request_id 単位で
INSERT を先に行う atomic-claim 方式のため、read-then-write の競合窓が無い。

照合不変条件(reconcile.ts が全口座で検査):

core.balance == shadow.available + shadow.reserved
                 + Σ(pending DEBIT) − Σ(pending CREDIT)

Foreign Key 戦略

参照整合性は 実際に強制・検証している。テスト D1(test/helpers/d1-mock.ts)
は PRAGMA foreign_keys = ON で起動する。db.batch() 内ではさらに
PRAGMA defer_foreign_keys = ON(COMMIT 時に一括検査)を設定し、子行が同一
バッチ内で親と一緒にコミットされる lane プリミティブの合成に対応する。
単発文は文ごとの即時検査となる。FK を本番相当に強制したうえで全テスト
(全テストスイート)が通ることを CI ゲートとする。

宣言している FK(構造的な所有関係)

子 列 親 備考
HtlcContracts txid Transactions(txid) insertTxWithLog で親と同一バッチ生成
GtidLegs gtid GtidTransactions(gtid)
GtidLegs txid Transactions(txid) Decision 後にバッチで backref
FxLegLocks gtid FxTransfers(gtid)
HtlcAuthRequests whitelist_id HtlcAuthWhitelist(whitelist_id)
DnsNetPositions cycle_id DnsCycles(cycle_id)
Attestation template_id ConditionTemplate(template_id)
Mandate parent_mandate_id Mandate(mandate_id) 自己参照(委任チェーン)
DebitMandate mandate_id Mandate(mandate_id) 継続収納契約の署名根拠
MandateBudget dd_mandate_id DebitMandate(dd_mandate_id) 枠は契約に従属する
ScheduledCollection dd_mandate_id DebitMandate(dd_mandate_id) 予告は契約に従属する
ScheduledCollection extra_mandate_id Mandate(mandate_id) 単発認可(追加認可)。NULL 可
CollectionAttempt collection_id ScheduledCollection(collection_id) 試行は予告に従属する

意図的に FK を貼らない列(理由つき)

  1. 監査・追記専用ログ(FinalityLog, TxEventLog, BankAuditLog,
    EntityStateLog)。FinalityLog.txid_or_gtid は ポリモーフィック
    (txid または gtid を取る)であり単一の親に FK できない。また親が論理的に
    消えても履歴は残す必要がある。
  2. HReservations.txid。H は 確定的に予測した txid に対して、その
    Transactions 行が生成される前に予約される
    (GTID/FX:
    src/zc/lanes/gtid/advance.ts で reserveH が insertTxWithLog に先行)。
    したがってこの列は前方参照であり、満たせる FK ではない。FK 強制によって
    この不変条件の不成立が観測されたため、明示的に非 FK とする。
  3. bank_id 系の論理識別子(Transactions.payer_bank_id 等)。参加者
    マスタへの参照だが、運用上の論理キーとして扱い、行ライフサイクルの所有関係
    ではないため FK 化しない。
  4. ScheduledCollection.txid。予告は Transactions を所有しない——予告
    が先に存在し、振替日に発火したものだけが取引を生む。したがってこの列は
    「発火したかどうか」を表す後書きの参照であり、行ライフサイクルの所有関係
    ではない。HtlcAuthRequests.txid(承認後に書かれる)と同じ扱いとする。

ON DELETE 句は付けていない(既定 = NO ACTION)。本システムは行を物理削除
しない(状態機械+追記ログ)ため、削除カスケードは不要であり、誤った削除は
制約違反として fail-closed させる方が安全である。

PDFを作成

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

用紙
組み方向
表紙
本文