第14巻 DBスキーマ定義(3) ― Watcher・Bank・レガシー連携・索引
目次
- ZC テーブル(WatcherObservation)
- WatcherObservation(外部レール確定イベントの観測記録)
- ZC テーブル(FinalityAnchor / FinalityCosign)
- FinalityAnchor(透明性アンカリング)
- FinalityCosign(参加行の副署)
- CosignPolicy(副署の必須化ポリシー)
- ZC テーブル(SystemMode)
- SystemMode(ZC全体のBCP縮退モード)
- HtlcContracts クロスチェーン拡張
- クロスチェーンHTLC(`HtlcContracts` の `cross_chain_*` / `onchain_*` 列)
- Bank基本テーブル
- BankAccounts(口座マスター)
- BankJournals(元帳:ゼロサム・INSERT ONLY)
- ZcRequests(ZC指示の冪等管理)
- SuspenseDetails(別段預金明細)
- DailyBalances(日次残高スナップショット)
- InterestRates(利率マスター)
- Bank テーブル(監査ログ・着金フィルタ)
- BankAuditLog(Bank側 コマンド監査ログ:INSERT ONLY)
- PaymentFilters(着金フィルタリングルール)
- PaymentApprovalRequests(着金承認待ちリクエスト)
- 初期データ
- ZC側
- Bank側
- Index Catalog
- Transactions
- FinalityLog
- HtlcContracts
- RtpRequests
- Cases / IdempotencyKeys / DnsCycles
- GtidLegs
- AccessAuditLog
- KeyRegistry
- Attestation
- Mandate
- DebitMandate
- ScheduledCollection
- CollectionAttempt
- WatcherObservation
- FinalityAnchor / FinalityCosign
- Legacy Adapter(対外接続系)
- Legacy Adapter テーブル(対外接続系)
- Foreign Key 戦略
- 宣言している FK(構造的な所有関係)
- 意図的に FK を貼らない列(理由つき)
第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()):
(source, external_ref, watcher_key_id)が既に記録済みなら、署名を再検証せず
既存行を返す(deduped: true)。同一 Watcher の重複観測は20_method_design.md§7.7.2 の冪等として
吸収する。一方、別 Watcher が同一イベントを報告した場合は dedup せず、検証して
1行追加する(n-of-m クォーラムへの1票)。- 未記録の場合、
buildWatcherObservationPayload()の正規ペイロード
(source,external_ref,venue,proof_type,issuer_ref)への
Watcher の署名をKeyRegistry(§K)で検証する(KEY_*/EXTERNAL_SIGNATURE_INVALID/SIGNATURE_REPLAYED)。 - 検証済み鍵の
owner_typeがEXTERNAL_RAILまたはATTESTERであること
(WATCHER_UNAUTHORIZED。それ以外の主体は Watcher として認めない)。 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()):
chain_idが co-signable(TX-*/GT-*・GTID-*/DNS-*)であり、participant_idが当該チェーンの当事者であること
(COSIGN_NOT_APPLICABLE。GLOBAL チェーン・非当事者は対象外)。- チェーンに最低1件の FinalityLog エントリがあること
(COSIGN_ENTRY_NOT_FOUND)。 {chain_id, entry_hash}への参加行の署名をKeyRegistry(§K)で検証
(KEY_*/EXTERNAL_SIGNATURE_INVALID/SIGNATURE_REPLAYED)。- 検証済み鍵が
owner_type='PARTICIPANT'かつowner_ref=participant_id
であること(COSIGN_PARTICIPANT_MISMATCH)。 (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つ:
- 手動: teller API
POST /bank/:bankId/v1/teller/suspense/:suspenseId/resolve(SETTLE/RETURN) - 自動: 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 を貼らない列(理由つき)
- 監査・追記専用ログ(
FinalityLog,TxEventLog,BankAuditLog,EntityStateLog)。FinalityLog.txid_or_gtidは ポリモーフィック
(txid または gtid を取る)であり単一の親に FK できない。また親が論理的に
消えても履歴は残す必要がある。 HReservations.txid。H は 確定的に予測した txid に対して、そのTransactions行が生成される前に予約される(GTID/FX:src/zc/lanes/gtid/advance.tsでreserveHがinsertTxWithLogに先行)。
したがってこの列は前方参照であり、満たせる FK ではない。FK 強制によって
この不変条件の不成立が観測されたため、明示的に非 FK とする。bank_id系の論理識別子(Transactions.payer_bank_id等)。参加者
マスタへの参照だが、運用上の論理キーとして扱い、行ライフサイクルの所有関係
ではないため FK 化しない。ScheduledCollection.txid。予告はTransactionsを所有しない——予告
が先に存在し、振替日に発火したものだけが取引を生む。したがってこの列は
「発火したかどうか」を表す後書きの参照であり、行ライフサイクルの所有関係
ではない。HtlcAuthRequests.txid(承認後に書かれる)と同じ扱いとする。
ON DELETE 句は付けていない(既定 = NO ACTION)。本システムは行を物理削除
しない(状態機械+追記ログ)ため、削除カスケードは不要であり、誤った削除は
制約違反として fail-closed させる方が安全である。