第17巻 API契約定義(3) ― Bank向けAPIと横断仕様
目次
- ZC→Bank Ingress API(13本・Bank Mockが実装)
- SettlementProofRef(`bank_proof_ref` の一般化)
- POST /bank/:bankId/zc-ingress/reserve-funds
- POST /bank/:bankId/zc-ingress/execute-debit
- POST /bank/:bankId/zc-ingress/execute-credit
- POST /bank/:bankId/zc-ingress/release-reserve
- POST /bank/:bankId/zc-ingress/leg-ready-check
- POST /bank/:bankId/zc-ingress/authority-check
- POST /bank/:bankId/zc-ingress/name-check
- POST /bank/:bankId/zc-ingress/account-verify
- POST /bank/:bankId/zc-ingress/credit-notify
- POST /bank/:bankId/zc-ingress/rtp-notify
- POST /bank/:bankId/zc-ingress/debit-settled
- POST /bank/:bankId/zc-ingress/initialize-bank
- POST /bank/:bankId/zc-ingress/cleanup-bank
- Bank 顧客API(顧客向け)
- Bank 行員API(行員向け)
- Bank 着金フィルタAPI
- 内部API(Cron用・外部公開しない)
- ダッシュボード・静的ページ
- リクエストトレーシング(横断仕様)
- 冪等性(横断仕様)
- メッセージ・スキーマ進化と互換性政策(横断仕様)
- エラーカタログ(横断仕様)
- カテゴリと HTTP ステータス対応
- 主要 reason_code 一覧
- 照会の認可(横断仕様) <a id="query-authorization"></a>
- リクエストヘッダ
- 参加者の主体認証(規範)
- 判定と応答
- 状態 reason_code(横断仕様) <a id="state-reason-code"></a>
- 位置づけ
- 主要な値(レーン・局面別)
- 命名規約(規範)
- 規範
第17巻 API契約定義(3) ― Bank向けAPIと横断仕様
本巻は
docs/specs/32_api_contracts.mdのZC→Bank Ingress API以降(Bank顧客/行員/着金フィルタAPI、内部API、冪等性・エラーカタログ等の横断仕様)を収める。前巻→第16巻の続き。
ZC→Bank Ingress API(13本・Bank Mockが実装)
SettlementProofRef(bank_proof_ref の一般化)
bank_proof_ref の型は BankProofRef/SettlementProofRef(同一型の別名)。proof_type の値域(実装の正: src/types/primitives.ts#ProofType:PAYER_EXEC_PROOF | PAYER_HV_ISOLATION_PROOF | PAYEE_EXEC_PROOF |NO_DEBIT_RECORDED_PROOF | ONCHAIN_ESCROW_LOCK_PROOF | ONCHAIN_RELEASE_PROOF |CREDIT_FAILED_PROOF | EXT_REFUND_PROOF)と
既存フィールド(issuer_bank_id, proof_id, recorded_at, custody_detail)は不変。
値の意味の全体像は 30_internal_design.md §12.3.3 を正とする。proof_type と venue は直交する別軸であり、SettlementProofRef は型の別名であってproof_type の値ではない(同§の規範)。以下の4フィールドを加法的
(すべて optional/nullable)に追加し、銀行勘定確認以外の証跡型
(日銀ネット/オンチェーン/第三者アテステーション)を同列に扱う。
{
"issuer_bank_id": "001",
"proof_type": "PAYEE_EXEC_PROOF",
"proof_id": "PROOF-...",
"recorded_at": "RFC3339",
"custody_detail": null,
"venue": "BANK_LEDGER",
"external_ref": null,
"signer_key_id": null,
"verified_at": null
}
venue:"BANK_LEDGER"(既定・参加行勘定確認)|"IGS_BOJ"(日銀ネット相当)
|"ONCHAIN"(オンチェーン移転証明)|"ATTESTATION"(第三者アテステーション)
|"CB_TOKEN"(トークン化中央銀行当座預金での確定)。
既存のcreateProof()(参加行勘定確認、src/shared/proof.ts)が生成する
proof は省略可(実質BANK_LEDGER)。CB_TOKENは非JPY通貨のファイナリティ・レール:各通貨の中央銀行(ECB / FedNY…)
がトークン化した当座預金をチェーン上で確定させる。ZC は外国中銀へ直接接続できない
ため、発行体の署名付き観測(source='CB_TOKEN:{中銀}:{チェーン}')をKeyRegistryで検証した証跡のみ受理する。トークン化JPYは BOJ クラシック当座に
追加するレール(src/shared/central_bank.ts)。external_ref: 当該 venue 内での参照(チェーン上の tx ハッシュ/IGS-ID/
アテステーションID)。venue=BANK_LEDGERでは未使用。signer_key_id: 証跡に署名した外部主体のKeyRegistry.key_id。verified_at: ZC がsrc/shared/external_signature.tsで署名検証に成功
した時刻(RFC3339)。
venue != "BANK_LEDGER" の証跡は src/shared/proof.ts のcreateSettlementProof() で生成する。このパスは内部でverifyExternalSignature() を呼ぶため、KeyRegistry 未登録・失効・署名
不正の場合は §エラーカタログの KEY_* / EXTERNAL_SIGNATURE_INVALID /SIGNATURE_REPLAYED を返す。
全エンドポイントは POST /bank/{bank_id}/zc-ingress/...
共通リクエストヘッダー(ZC の egress 署名は二重受け。X-ZC-Key-Id の有無で判別):
# 非対称署名パス(新方式・優先): X-ZC-Key-Id が在るときこちらが選択される
X-ZC-Key-Id: KEY-... # KeyRegistry の署名鍵 key_id(owner_type='ZC')
X-ZC-Sig-Nonce: string # 鍵ごとに一意な nonce(リプレイ防止)
X-ZC-Sig-Time: RFC3339 # 署名時刻
X-ZC-Signature: base64 # buildSignedMessage(...) への署名
# HMAC パス(旧方式・後方互換): X-ZC-Key-Id が無いときのフォールバック
X-ZC-Signature: HMAC-SHA256-hex
X-Idempotency-Key: string # 参考。ingress の冪等判定はボディの request_id(ZcRequests)で行う
Content-Type: application/json
検証は handleBankIngressHttp(src/bank/ingress.ts)が行い、X-ZC-Key-Id
ヘッダーの有無で非対称署名(verifyZcSignature、src/shared/zc_signature.ts)と
HMAC(verifySignature、src/shared/hmac.ts)を判別する。いずれも失敗時は401 INVALID_SIGNATURE、HMAC パスで署名欠落時は 401 MISSING_SIGNATURE。この
段階的二重受けは §メッセージ・スキーマ進化と互換性政策 で述べる移行パターンの実例である。
POST /bank/:bankId/zc-ingress/reserve-funds
H_RESERVED確保要求
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"amount": { "value": 1200, "currency": "JPY" },
"account_hash": "h:..."
}
Response OK: { "result": "RESERVED", "reservation_ref": "uuid" }
Response NG: { "result": "ERROR", "reason_code": "INSUFFICIENT_FUNDS" }
POST /bank/:bankId/zc-ingress/execute-debit
a実行指示(PayerExecRequested準拠、30_internal_design.md §13.2)
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"amount": { "value": 1200, "currency": "JPY" },
"decision_proof_ref": "DP-...",
"h_reservation": { "reservation_id": "H-...", "mode": "RESERVED" },
"execution_deadline": "RFC3339",
"lane": "EXPRESS|...",
"payer_account_hash": "h:..."
}
Response: { "result": "OK", "bank_proof_ref": { "issuer_bank_id": "001", "proof_type": "PAYER_EXEC_PROOF", "proof_id": "...", "recorded_at": "RFC3339" } }
※ HIGH_VALUEレーンは reserve-funds を経由しないため payer_account_hash を直接渡す
POST /bank/:bankId/zc-ingress/execute-credit
b実行指示(PayeeExecRequested)
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"amount": { "value": 1200, "currency": "JPY" },
"decision_proof_ref": "DP-...",
"payee_account_hash": "h:..."
}
Response: { "result": "OK", "bank_proof_ref": { "issuer_bank_id": "002", "proof_type": "PAYEE_EXEC_PROOF", "proof_id": "...", "recorded_at": "RFC3339", "custody_detail": null } }
※ Custody発生時: "custody_detail": { "is_custody": true, "reason_code": "ACCOUNT_CLOSED", "custody_account_ref": "..." }
POST /bank/:bankId/zc-ingress/release-reserve
H_RESERVED解放
Request: { "request_id": "uuid", "txid": "TX-...", "reservation_ref": "uuid" }
Response: { "result": "RELEASED", "reservation_ref": "uuid" }
POST /bank/:bankId/zc-ingress/leg-ready-check
GTID事前レディネス確認
Request:
{
"request_id": "uuid",
"gtid": "GT-...",
"leg_id": "L1",
"role": "PAYER|PAYEE",
"amount": { "value": 3000, "currency": "JPY" },
"account_hash": "h:..."
}
Response: { "result": "OK" } または { "result": "NG", "reason_code": "INSUFFICIENT_FUNDS" }
POST /bank/:bankId/zc-ingress/authority-check
AML/制裁スクリーニング
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"check_type": "INITIAL|RECHECK",
"vault_ref": "optional"
}
Response: { "result": "OK" } または { "result": "NG", "reason_code": "SANCTIONS_MATCH" }
POST /bank/:bankId/zc-ingress/name-check
名義確認
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"pspr_ref": "optional",
"account_hash": "h:..."
}
Response: { "result": "MATCH" } または { "result": "MISMATCH", "reason_code": "NAME_MISMATCH" }
POST /bank/:bankId/zc-ingress/account-verify
口座確認(ZCからBankへ照会)
Request:
{
"request_id": "uuid",
"verification_id": "V-...",
"target_account_hash": "h:...",
"target_account_name": "佐藤 花子"
}
Response: { "result": "MATCHED|MISMATCHED|NOT_FOUND", "match_score": 1.0, "name_provided": "...|null", "fraud_warning": false }
match_scoreは完全一致 1.0 / 編集距離 1 以内 0.8(表記ゆれ吸収)/ それ以外 0.0。- 照合対象の名義(
name_provided)は要求側が渡した文字列をそのまま返す。行内に保管された
名義そのもの(かつて本節がactual_nameとして記載していたもの)は返さない——
返せば、口座番号を総当たりして名義を収集する経路になる。したがって ZC 側のAccountVerifications.target_account_nameは本経路では埋まらない。 target_account_nameを省略した場合は口座存在確認のみとなりMATCHED/match_score: 1.0。- 本節の項目名は ZC 側
POST /api/account-verifyの項目名と一致しない(ZC がtarget_account_id/name_to_verifyを受け、境界でtarget_account_hash/target_account_name
へ写す)。両端の型はsrc/types/api/bank-ingress.ts#BankAccountVerifyIngressRequest/#BankAccountVerifyIngressResponseを唯一の宣言とし、呼び手(src/zc/directory/account_verify.ts)と
受け手(src/bank/ingress/verify.ts)の双方がこれを import する——両端が別々に同名の型を
宣言していた間、項目名がずれていても型検査が通り、当コマンドは口座を解決できないまま
出荷されていた。
POST /bank/:bankId/zc-ingress/credit-notify
入金結果通知
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"payee_account_hash": "...",
"amount": { "value": 1200, "currency": "JPY" },
"payer_bank_id": "001",
"payer_name_masked": "タ●●",
"purpose": "P2P"
}
Response: { "result": "NOTIFIED" }
POST /bank/:bankId/zc-ingress/rtp-notify
RTP請求通知(payee → payer bank)
Request:
{
"request_id": "uuid",
"rtp_id": "RTP-...",
"payee_bank_id": "001",
"payer_bank_id": "002",
"amount": { "value": 2000, "currency": "JPY" },
"expires_at": "RFC3339",
"payee_name": "田中商店",
"description": "optional"
}
Response: { "result": "NOTIFIED" }
POST /bank/:bankId/zc-ingress/debit-settled
決済完了通知(払出行=発起行向け)。ZC が TX を SETTLED まで確定させた後に送り、
着金が相手方へ届き end-to-end で最終確定したことを知らせる(全銀将来ビジョン
入金結果通知の払出側)。行内では監査証跡(BankAuditLog)を残すのみ。冪等。
Request:
{
"request_id": "uuid",
"txid": "TX-...",
"amount": { "value": 1200, "currency": "JPY" },
"payee_bank_id": "002",
"settled_at": "RFC3339"
}
Response: { "result": "ACKNOWLEDGED", "txid": "TX-..." }
POST /bank/:bankId/zc-ingress/initialize-bank
参加行側の勘定・仕訳の初期化。ZC は参加行登録(Participants)のみを行い、行内勘定
(別段預金 SUSPENSE/ZC清算勘定 SETTLEMENT/現金 ASSET/利益剰余金 EQUITY/日銀預け金
BOJ)と初期仕訳は各行が用意する(設計原則: 金融機関は既存の責務を保持)。冪等
(別段預金が既存なら ALREADY_INITIALIZED)。
Request: { "request_id": "optional uuid", "boj_prefund": 100000000000 }(boj_prefund 省略時は1000億円のBOJプレファンド)
Response: { "result": "INITIALIZED"|"ALREADY_INITIALIZED", "bank_id": "001" }
POST /bank/:bankId/zc-ingress/cleanup-bank
参加行離脱時の行内データ(勘定・仕訳・金利設定・日次残高)削除。ZC側データ
(Participants・ZcRequests・SuspenseDetails 等)は別途ZCが削除する。
Request: {}(ボディ不要)
Response: { "result": "CLEANED_UP", "bank_id": "001" }
Bank 顧客API(顧客向け)
共通ヘッダー: X-Bank-Id: 001, X-Customer-Id: customer_uuid(モック用・認証なし)
GET /bank/:bankId/v1/me/accounts
口座一覧
GET /bank/:bankId/v1/me/accounts/:accountId/balance
残高照会 → { "account_id": "...", "balance": 980000, "currency": "JPY", "as_of": "RFC3339" }
GET /bank/:bankId/v1/me/accounts/:accountId/transactions
取引履歴
POST /bank/:bankId/v1/me/transfers
振込実行(全ZCレーン対応)
Request:
{
"amount": { "value": 5000, "currency": "JPY" },
"payee_bank_id": "002",
"payee_account_hash": "h:...",
"payee_account_id": "0020000001",
"lane": "STANDARD",
"purpose": "P2P",
"idempotency_key": "uuid",
"payer_account_id": "optional"
}
payee_account_id 指定時は payee_bank_id を口座番号先頭3桁から自動導出可能。
GET /bank/:bankId/v1/me/transfers/:txid
振込状態照会
GET /bank/:bankId/v1/me/approvals
着金承認リクエスト一覧
Query: ?account_id=...&status=PENDING
POST /bank/:bankId/v1/me/approvals/:approvalId/respond
着金承認への応答
Request: { "approved": true }
承認時はZCにresume_creditを通知(Queue経由)。
Bank 行員API(行員向け)
共通ヘッダー: X-Bank-Id: 001, X-Teller-Id: teller_id(モック用・認証なし)
POST /bank/:bankId/v1/teller/cash/deposit
現金入金
POST /bank/:bankId/v1/teller/cash/withdrawal
現金払い戻し
GET /bank/:bankId/v1/teller/accounts
口座一覧(行員用)
POST /bank/:bankId/v1/teller/accounts
口座作成
POST /bank/:bankId/v1/teller/accounts/batch
口座一括作成
PATCH /bank/:bankId/v1/teller/accounts/:accountId/status
口座ステータス更新(NORMAL/FROZEN/CLOSING_HOLD/CLOSED)
GET /bank/:bankId/v1/teller/accounts/:accountId/journals
口座の仕訳照会
GET /bank/:bankId/v1/teller/journals
全仕訳照会(行全体)
GET /bank/:bankId/v1/teller/suspense
別段預金一覧
POST /bank/:bankId/v1/teller/suspense/:suspenseId/resolve
別段預金収束処理(Custody解消等)
GET /bank/:bankId/v1/teller/batch/status
バッチ処理状態照会
GET /bank/:bankId/v1/teller/audit-log
監査ログ照会
Query: ?txid=TX-...&limit=100
Bank 着金フィルタAPI
GET /bank/:bankId/v1/filters
フィルタ一覧
Query: ?account_id=...
POST /bank/:bankId/v1/filters
フィルタ作成
Request:
{
"scope": "ACCOUNT",
"account_id": "0010000001",
"filter_type": "AMOUNT_LIMIT|SENDER_BLOCK|SENDER_BANK_BLOCK|EDI_PATTERN|REQUIRE_APPROVAL",
"condition": { "max_amount": 50000 },
"action": "REJECT|HOLD_CONFIRM|HOLD_MANUAL",
"description": "5万円超の着金を承認制に"
}
値域の正(実装): src/types/states.ts#FilterType: AMOUNT_LIMIT | SENDER_BLOCK |SENDER_BANK_BLOCK | EDI_PATTERN | REQUIRE_APPROVAL /src/types/states.ts#FilterAction: REJECT | HOLD_CONFIRM | HOLD_MANUAL。
DELETE /bank/:bankId/v1/filters/:filterId
フィルタ削除
PATCH /bank/:bankId/v1/filters/:filterId
フィルタ有効/無効切替
Request: { "is_active": false }
内部API(Cron用・外部公開しない)
X-Cron-Secret ヘッダー検証必須。
規範(管理シークレットの取扱い):
CRON_SECRETの照合は 定数時間比較(
timingSafeEqualStr)で行い、応答タイミングからの逐次的な秘密復元を防ぐ。ヘッダ欠落・環境側未設定はいずれも fail-closed(403)で拒否し、空文字どうしの
一致を通過扱いにしてはならない。また
CRON_SECRETを クライアントへ配信される成果物
(ダッシュボード HTML/JS 等)へ埋め込んではならない。埋め込むと view-source で全
/internal/*操作の認可を回避できてしまう。運用ダッシュボードは秘密を実行時に運用者から取得しsessionStorage 等に保持する(実装
src/router/internal.ts、src/dashboard/*.html、回帰
test/invariants/dashboard_secret_guard.test.ts)。
GET /internal/metrics
運用メトリクス(権威状態の読み取り専用射影)。既定は Prometheus テキスト展開形式
(Content-Type: text/plain; version=0.0.4)、?format=json で JSON を返す。書き込みを
行わないため read-only 縮退中でもスクレイプ可能。実装 src/zc/platform/metrics.ts。
POST /internal/cron/eod
EODバッチ手動トリガー
POST /internal/cron/timeout-sweep
タイムアウト巡回手動トリガー
POST /internal/cron/finality-audit
FinalityLog ハッシュチェーン全鎖監査の手動トリガー(通常は EOD バッチに内包)。断絶検知時は CASE へ収束。
Response: { "chains_checked": N, "entries_checked": M, "broken_chains": [...], "cases_opened": K }
POST /internal/seed
初期データ投入(開発用)
POST /internal/dns/kick
DNS手動キック
Request: { "business_date": "YYYY-MM-DD" }(省略時は当日)
POST /internal/dns/settle
DNS手動清算
Request: { "cycle_id": "..." }
POST /internal/dns/intraday-cutoff
日中カットオフ(24/365)。現行の OPEN ウィンドウを締め(kick+settle)、次の日中サイクルを
開く。1日複数回の清算=ローリング清算を可能にする(runIntradayDnsCutoff)。
Request: { "business_date": "YYYY-MM-DD", "currency": "JPY" }(いずれも省略可。既定は当日・JPY。サイクルIDは DNS-{CCY}-YYYYMMDD-NN)
POST /internal/dns/resume
DNS_HOLD(ネット債務者の残高不足による中断)からの再開。ブリッジ流動性の補填後に呼び、resumeDns が当座残高を再チェックして清算を再試行する(10_requirements.md §3.2.5.2 の制度プロトコル)。
Request: { "cycle_id": "..." }
GET /internal/boj-positions
各銀行の日銀預け金勘定(BOJ)残高照会
POST /internal/sim/setup
シミュレーター大規模初期化(20行×200口座)
POST /internal/sim/setup-bank
シミュレーター単行セットアップ
POST /internal/transfers/:txid/resume-credit
着金承認後のクレジット処理再開通知
ダッシュボード・静的ページ
| パス | 内容 |
|---|---|
/ /dashboard |
メインダッシュボード(取引一覧・状態可視化) |
/console |
オペレーションコンソール(Alpine.js + ECharts) |
/bank-app |
顧客向け銀行アプリ(Alpine.js) |
/theater /theatre |
Settlement Theater — 状態遷移のアニメーション再生 |
/sky |
Sky モード(システム俯瞰ビュー) |
リクエストトレーシング(横断仕様)
全 HTTP レスポンスは X-Request-Id ヘッダーを必ず返す。エラー時は
レスポンス本文の request_id フィールドにも同値が入り、構造化ログ
(後述)と突合可能になる。
- リクエスト側が
X-Request-Idを付与した場合、その値をそのまま採用する。 - 付与が無い場合、サーバが
req-<uuid>を生成して返す。 - Cloudflare Queues コンシューマでも同等の logger を初期化し、
request_id,message_type,txid,gtid,attemptを 1 行 JSON としてconsole
に出力する(Logpush でそのままパース可能)。
実装: src/shared/logger.ts を参照。詳細はdocs/specs/30_internal_design.md § Observability に集約。
冪等性(横断仕様)
idempotency_key を受け取る全エンドポイント(/api/transfers、/api/gtid/register、/api/rtp/request、/api/htlc/*、/api/fx/transfers など)は、同一キーの再送に対して保存済みの応答を
そのまま返す。ただし HTTP ステータスコードは初回と異なる場合がある
(例: HTLC create は初回 201、再送では 200)。
同一キーが異なるリクエストボディで再使用された場合は、保存済み応答
を返さず 409 IDEMPOTENCY_KEY_CONFLICT を返す。ボディの同一性は
リクエスト全体の SHA-256 ハッシュで判定する(src/shared/idempotency.ts
の resolveIdempotency())。これにより、クライアントが同じキーを使い回し
つつ金額や payee だけ変えてしまうといった実装ミスをしても、古い応答が
意図せずそのまま返ってしまったり、二重実行と誤認されたりすることを防げる。
署名のリプレイ防止など、ボディ比較を伴わない単発キー(例:sig:{key_id}:{nonce})はこの判定の対象外(ハッシュ未保存、常に
非衝突)。
実装: src/shared/idempotency.ts。
メッセージ・スキーマ進化と互換性政策(横断仕様)
参加者は N 行いても同時にはアップグレードしない。したがって電文・API 契約には
明示的な進化政策が要る。これが無いまま本番へ行くと、最初のスキーマ改訂が最初の
全網障害になりかねない。実在の決済レールの電文改訂が「年次サイクル・数年前告知」
という重さで動くのは、この「参加者全員が同時にはアップグレードできない」という
問題への答えである。ZC も同じ規律を採る。
方針(3点)
- n / n-1 併存受理: サーバは常に現行スキーマ
nと直前n-1の 2 世代を同時に
受理する。リクエストはschema_version(PaymentInitiatedの必須フィールド。永続化列はTransactions.schema_version、31_schema.md § Transactions、既定'1.0')で自世代を自己申告する。
新フィールドは加法的・任意(省略時は従来動作)として入れ、破壊的変更は世代境界で
のみ行う。 - 公表された非推奨期限:
n-1を受理し続ける期限を事前に公表する。告知から期限までは
全参加行が移行できるだけの猶予(実在レール同様、複数年規模)を置く。 - 強制移行手続: 期限到来後、
n-1のリクエストは受理を停止し明示拒否する
(サイレントに古い解釈へ倒さない)。移行が滞る参加行には期限前に個別告知し、接続
認定試験でn適合を確認したうえで切替える。
すでに一度やっている実例(署名方式の段階的二重受理)
この「段階的二重受理」を ZC は署名方式の移行で一度通している。ZC の egress 署名は
歴史的に共有 HMAC だったが、非対称署名(KeyRegistry の公開鍵、owner_type='ZC')へ
移行した。検証側(bank ingress、src/bank/ingress/)はX-ZC-Key-Id ヘッダーの有無
だけで新旧を判別する。ヘッダーが在れば非対称パス(verifyZcSignature、src/shared/zc_signature.ts)、無ければ従来の HMAC パスへフォールバックする(§ZC→Bank
Ingress API の共通ヘッダー参照)。送信側は世代をまたいで切り替えられ、受信側は両方を
無停止で受ける。これは上記「n / n-1 併存受理」を署名という一断面で具体化したものに
ほかならない。
電文スキーマ全体にはこの発想をそのまま一般化する:「新方式の存在を示す自己申告
(署名では X-ZC-Key-Id の有無、電文では schema_version)で世代を判別し、告知した
期限まで両受け、期限で強制移行」。署名で正しく機能しているこのパターンを、契約
バージョニングの既定手続として明文化しておく。
エラーカタログ(横断仕様)
全エンドポイントは失敗時に以下の JSON 形を返す。reason_code は機械可読、category は HTTP ステータスとリトライ可否を一元化する。
{
"error": "human-readable message",
"reason_code": "H_LIMIT_EXCEEDED",
"category": "CONFLICT",
"details": { "txid": "TX-001", "requested": 1000, "available": 500 },
"request_id": "req-..."
}
カテゴリと HTTP ステータス対応
src/shared/errors.ts の httpStatusOf() がただ一つの真。
| Category | HTTP | Retryable | 用途 |
|---|---|---|---|
| VALIDATION | 400 | ✗ | 入力不正、フォーマット違反、FATF R.16 違反 |
| AUTH | 401 | ✗ | API キー欠落・HMAC 不一致・Whitelist 拒否 |
| NOT_FOUND | 404 | ✗ | TX/HTLC/GTID/RTP/Account/Proxy/Participant 不在 |
| CONFLICT | 409 | ✗ | 状態ガード・楽観ロック衝突・H 上限超過・名義不一致・サーキット開放 |
| RATE_LIMIT | 429 | ✓ (backoff) | レート制限 |
| INVARIANT | 500 | ✗ | 不変条件違反(バグ)。FinalityLog 改ざん検出、二重記帳など |
| INTERNAL | 500 | ✗ | 未分類例外。実装漏れ |
| DOWNSTREAM | 502 | ✓ | 銀行 / IGS / 外部呼び出しの一時的失敗 |
| TIMEOUT | 504 | ✓ | 銀行 / IGS のタイムアウト |
Queue コンシューマは Retryable=✓ のみ msg.retry() し、それ以外はmsg.ack() してケース化する(無限ループ防止)。
422 (Unprocessable Entity) について: 上表の
category → HTTP写像は
httpStatusOf()(DomainError経由)の対応であり 422 を含まない。一方、一部のエンドポイントは業務ルール違反を
httpStatusOf()を介さず 422 で直接返す(
FOUR_EYES_REQUIRED/EVIDENCE_REQUIRED/A_OR_B_CONFIRMED(
POST /api/.../h-unlock-authorize)、FOUR_EYES_REQUIRED/EVIDENCE_REQUIRED/
B_CONFIRMED/WINDOW_EXPIRED/NOT_CORRECTABLE(
POST /api/transfers/:txid/misrecord-correct)、OVER_REVERSAL(POST /api/reversals)、
AMOUNT_BALANCE_MISMATCH(GTID advance)など)。これらは「構文は妥当だが現在の業務状態では処理不能」を表す意図的な 422 であり、
category写像の対象外である点に注意(
categoryフィールドにはVALIDATIONを付す)。
金額の上限について: 金額系フィールド(
amount.value/ QRamount/銀行テラー
amount/initial_deposit等)は、各 ingress の入力境界で
MAX_AMOUNT_VALUE(src/shared/constants.ts、1兆=1_000_000_000_000)を超えると
INVALID_AMOUNTで拒否される。これは業務上の上限(tx_amount_limit/
daily_amount_limit/hv_thresholdは別途 Participants 等で管理)ではなく、FXレート乗算など下流の数値演算が
Number.MAX_SAFE_INTEGERに近づいて精度劣化することを防ぐための、不正・異常入力に対するセーフティネットである。
適用対象:
POST /api/transfers、POST /api/htlc/create、
POST /api/gtid/register(leg単位)、POST /api/rtp/request、
POST /api/fx/quote・POST /api/fx/transfers、QRgenerate、銀行テラー
cash/deposit・cash/withdrawal・口座開設initial_deposit。
主要 reason_code 一覧
| reason_code | category | 発生箇所例 |
|---|---|---|
| INVALID_REQUEST | VALIDATION | バリデーション全般 |
| MISSING_FIELD | VALIDATION | 必須フィールド欠落 |
| INVALID_AMOUNT | VALIDATION | 金額が負・非整数・上限超過(amount.value は 1兆 (MAX_AMOUNT_VALUE、src/shared/constants.ts) を超えると拒否される) |
| INVALID_CURRENCY | VALIDATION | amount.currency が非対応(POST /api/transfersはJPY固定、POST /api/gtid/registerはJPY/USD/EUR/GBP/CHF。上記「GTID register」本文の許可通貨が正) |
| INVALID_LANE | VALIDATION | 未知の lane 値 |
| INVALID_STATE | VALIDATION | クライアント側からの不正な状態指定 |
| INVALID_PROXY_TYPE | VALIDATION | Proxy 解決時 |
| FATF_R16_VIOLATION | VALIDATION | クロスボーダーの FATF データ不備 |
| PREIMAGE_MISMATCH | VALIDATION | HTLC claim 時に hashlock 不一致 |
| EXPIRED | VALIDATION | RTP・HTLC・QR の有効期限超過 |
| UNAUTHORIZED | AUTH | API キー欠落 |
| INVALID_HMAC | AUTH | ZC↔Bank HMAC 検証失敗 |
| WHITELIST_REJECTED | AUTH | HTLC Auth Whitelist で拒否 |
| ACCOUNT_FROZEN | AUTH | 口座凍結中の操作 |
| TX_NOT_FOUND | NOT_FOUND | GET /api/transactions/:txid などで該当無し |
| HTLC_NOT_FOUND | NOT_FOUND | HTLC 操作対象不在 |
| GTID_NOT_FOUND | NOT_FOUND | GTID 操作対象不在 |
| RTP_NOT_FOUND | NOT_FOUND | RTP 操作対象不在 |
| ACCOUNT_NOT_FOUND | NOT_FOUND | 口座照会失敗 |
| PROXY_NOT_FOUND | NOT_FOUND | Proxy 解決失敗 |
| PARTICIPANT_NOT_FOUND | NOT_FOUND | 参加行未登録 |
| CONCURRENCY_CONFLICT | CONFLICT | 楽観ロック競合(transitionWithLog strict モード) |
| STATE_GUARD | CONFLICT | 状態ガードで遷移不可 |
| IDEMPOTENCY_REPLAY | CONFLICT | 同一 idempotency_key の再送(既存応答返却) |
| IDEMPOTENCY_KEY_CONFLICT | CONFLICT | 同一 idempotency_key を異なるリクエストボディで再使用(409、既存応答は返さない) |
| ALREADY_PROCESSED | CONFLICT | 既処理イベントの再投入 |
| H_LIMIT_EXCEEDED | CONFLICT | H 予約が h_limit を超過(JPYはParticipants、非JPYはParticipantCurrencyLimits) |
| RESERVE_FAILED | CONFLICT | 銀行側 reserve-funds が NG |
| AUTHORITY_CHECK_NG | CONFLICT | 銀行側 authority-check が NG |
| NAME_MISMATCH | CONFLICT | 名義不一致 |
| CIRCUIT_OPEN | CONFLICT | CircuitBreaker が OPEN |
| BANK_ERROR | DOWNSTREAM | 銀行 ingress が 5xx |
| BANK_TIMEOUT | TIMEOUT | 銀行 ingress が応答遅延 |
| IGS_ERROR | DOWNSTREAM | IGS コールバック失敗 |
| ALS_LOOKUP_FAILED | DOWNSTREAM | ALS(Account Lookup Service)失敗 |
| RATE_LIMITED | RATE_LIMIT | レート上限 |
| LEGACY_ADAPTER_REQUEST_IN_FLIGHT | DOWNSTREAM | Legacy adapter: 同一 request_id が別の呼出しで処理中(src/bank/legacy/adapter.ts) |
| MANDATE_NOT_FOUND | NOT_FOUND | 委任が存在しない(失効操作・スコープ照合) |
| DD_MANDATE_NOT_FOUND | NOT_FOUND | 継続収納契約が存在しない |
| COLLECTION_NOT_FOUND | NOT_FOUND | 収納予告が存在しない |
| CHARGE_REF_ALREADY_COLLECTED | CONFLICT | 当該費目は既に CONFIRMED_OK(二重収納の防止) |
| CHARGE_REF_INVALID | VALIDATION | PERIODIC の構造検証違反、または PR-DD-PERIOD-AHEAD-MAX 超過 |
| BUDGET_RATE_EXCEEDED | CONFLICT | リセット型の累計枠を超過(時が経てば再び使える) |
| BUDGET_EXHAUSTED | CONFLICT | 消尽型の累計枠を使い切った(契約の総量に達した=契約範囲外) |
| CAP_EXCEEDS_POLICY | VALIDATION | 契約が宣言した上限が制度上限(PR-DD-*)を超える |
| SIGNATURE_REQUIRED_FOR_RAISE | AUTH | 上限の引き上げに顧客署名がない(引き下げには不要) |
| FROZEN_UNFAVOURABLE_CHANGE | CONFLICT | 凍結後の不利益変更(増額・前倒し)。減額・取下げは可 |
| NOTICE_PERIOD_TOO_SHORT | VALIDATION | 予告期間が契約の notice_days_min に満たない |
| LADDER_MAX_EXCEEDED | VALIDATION | ラダーの段数が上限を超える |
| LADDER_RUNG_CANNOT_BE_REALTIME | VALIDATION | 第 2 段以降を REALTIME にはできない |
| LATEFEE_EXCEEDS_POLICY | VALIDATION | 遅延損害金が率または絶対額の上限を超える |
| REALTIME_NOT_PERMITTED | AUTH | 即時収納は既定で不許可(開放は制度判断による) |
| MODE_UNSUPPORTED_BY_PAYER_BANK | CONFLICT | 払出行のプロファイルが要求モードを提供できない |
| CHAIN_TAMPERED | INVARIANT | FinalityLog のハッシュチェーン検証失敗 |
| LEDGER_IMBALANCE | INVARIANT | Bank 仕訳の借方=貸方が崩れた |
| IMPOSSIBLE_TRANSITION | INVARIANT | ALLOWED_TRANSITIONS 不在の遷移 |
| OWNERSHIP_VIOLATION | INVARIANT | 単一所有者則違反: 行を所有しない当事者が状態遷移を発行(strict:falseでも降格されず無条件throw、src/shared/errors.ts / 30_internal_design.md#single-owner) |
| KEY_NOT_FOUND | NOT_FOUND | KeyRegistry に key_id が存在しない(外部署名検証) |
| KEY_REVOKED | AUTH | occurred_at が鍵の revoked_at 以降(外部署名検証) |
| KEY_EXPIRED | AUTH | 鍵が status!=ACTIVE、または有効期間外(外部署名検証) |
| EXTERNAL_SIGNATURE_INVALID | AUTH | KeyRegistry の公開鍵での署名検証失敗 |
| SIGNATURE_REPLAYED | CONFLICT | 同一 (key_id,nonce) の再使用(外部署名検証) |
| TIMESTAMP_SKEW | VALIDATION | occurred_at が許容スキューを超過(外部署名検証) |
| TEMPLATE_NOT_WHITELISTED | AUTH | ConditionTemplate が存在しないか ACTIVE でない |
| ATTESTER_UNAUTHORIZED | AUTH | 鍵が allowed_attester_scope の範囲外(アテステーション) |
| ATTESTATION_INVALID | VALIDATION | statement_hash が sha256 hex 形式でない |
| ATTESTATION_EXPIRED | VALIDATION | occurred_at から TTL(既定60分)を超過したアテステーションを利用しようとした |
| MANDATE_NOT_FOUND | NOT_FOUND | Mandate(または委任チェーンの祖先)が存在しない |
| MANDATE_BREACH | AUTH | amount/purpose/lane がチェーン中いずれかのリンクの許可範囲外 |
| MANDATE_EXPIRED | AUTH | now がいずれかのリンクの [valid_from, valid_to) 範囲外 |
| MANDATE_REVOKED | AUTH | いずれかのリンクが revoked_at 以降(失効は遡及しない) |
| WATCHER_UNAUTHORIZED | AUTH | 署名検証済み鍵の owner_type が EXTERNAL_RAIL/ATTESTER でない |
| ANCHOR_NOT_FOUND | NOT_FOUND | FinalityAnchor に anchor_id が存在しない |
| CHAIN_NOT_ANCHORED | NOT_FOUND | アンカー時点で当該チェーンが存在しなかった |
| COSIGN_ENTRY_NOT_FOUND | NOT_FOUND | 副署対象チェーンに FinalityLog エントリが無い(GENESIS) |
| COSIGN_BASIS_NOT_FOUND | NOT_FOUND | 副署の基準エントリがまだ無い(不可逆点のエントリも、当該チェーンを含むアンカーも存在しない)。tip へフォールバックすると定足数が成立しなくなるため、エラーとして返す |
| COSIGN_PARTICIPANT_MISMATCH | AUTH | 署名検証済み鍵の owner が participant_id と一致しない |
| COSIGN_NOT_APPLICABLE | VALIDATION | GLOBAL/未分類チェーン、または当該チェーン(TX/GTID/DNS)の当事者でない参加行への副署 |
| SYSTEM_BCP_READ_ONLY | DOWNSTREAM | ZCが BCP_READONLY(ベンダー障害縮退モード)中で新規の資金移動を受理できない |
| SYSTEM_QUORUM_LOSS_READ_ONLY | DOWNSTREAM | ZCが QUORUM_LOSS_READONLY(設計原則10の自動縮退、合意ログ quorum 喪失)中で状態確定を受理できない。回復で自動解除されるため queue は retry で保持 |
| ONCHAIN_TIMELOCK_INVALID | VALIDATION | cross_chain.onchain_timelock が timelock 以降(ZC側外側タイムロックが先に切れる) |
| ONCHAIN_CHAIN_CLASS_REQUIRED | VALIDATION | cross_chain 指定時に onchain_chain_class が無い(確定種別が無いと Watcher 定足数の既定が最弱に落ちるため) |
| NOT_CROSS_CHAIN | VALIDATION | cross-chain-lock/onchain-fulfillment を cross_chain_source が NULL のHTLCに対して呼んだ |
| ONCHAIN_PROOF_MISMATCH | VALIDATION | onchain-fulfillment の preimage が hashlock に一致しない |
| ONCHAIN_TIMEOUT | VALIDATION | クロスチェーンHTLCの onchain_timelock 超過(DECIDED_CANCELへ遷移) |
| CONDITION_TEMPLATE_NOT_SET | VALIDATION | claim-by-attestation 対象のHTLCに condition_template_id が設定されていない(プログラマビリティ) |
| TEMPLATE_MISMATCH | VALIDATION | claim-by-attestation の template_id がHTLCの condition_template_id と一致しない(プログラマビリティ) |
| ATTESTATION_NOT_PASS | VALIDATION | アテステーションの verified_result が PASS でない(HtlcClaimRejectedを記録) |
| COUNTERPARTY_WINDOW_CLOSED | CONFLICT | EXPRESS精査時に相手行(payee)の稼働ウィンドウが閉じている。PRECHECKED_SUSPENDEDへ一時停止し、ウィンドウ再開後にタイムアウトスイープが自動再開する(稼働ウィンドウ) |
| ELIGIBILITY_NOT_ATTESTED | AUTH | createAuthRequest():ホワイトリストに eligibility_template_id が設定されているが、eligibility_attestation が未提供/verified_result!=='PASS'/検証失敗(受取側起点オーソリ) |
| PURPOSE_VIOLATION | AUTH | captureHtlcAuth():HtlcAuthRequests.purpose がホワイトリストの allowed_purposes に含まれない(受取側起点オーソリ) |
| PROOF_SOURCE_UNTRUSTED | AUTH | assertTrustedSettlementProof():venue!=='BANK_LEDGER' の SettlementProofRef に signer_key_id/verified_at が無い=署名検証済みでない(決済証跡の信頼アンカー) |
| CONDITION_EXPR_INVALID | VALIDATION | condition_expr_json の式木が構造検証に失敗(POST /api/htlc/create・claim-by-conditions・POST /api/conditions/*) |
| CONDITION_EXPR_NOT_SET | VALIDATION | claim-by-conditions 対象の HTLC に condition_expr_json が設定されていない |
| CONDITIONS_NOT_MET | VALIDATION | 条件式が不成立(状態は HTLC_LOCKED のまま。HtlcConditionsEvaluated を証跡化) |
| ATTESTATION_EQUIVOCATION | CONFLICT | 同一 (template, subject) に PASS/FAIL が混在(fail-closed。CASE へ収束) |
| WATCHER_EQUIVOCATION | CONFLICT | 同一 (source, external_ref) に矛盾する Watcher 観測(WatcherEquivocationDetected を証跡化し CASE へ収束。20_method_design.md §7.7.2-4) |
| ONCHAIN_QUORUM_PENDING | VALIDATION | Watcher 定足数(onchain_min_watchers)未達。HTLC_ONCHAIN_PENDING に留まり OnchainQuorumPending を証跡化(20_method_design.md §7.7.2-2) |
| ONCHAIN_INSUFFICIENT_CONFIRMATIONS | VALIDATION | 確認深度ゲート未通過。深度は定足数を構成する相異なる運用主体の申告の最小値を採る(20_method_design.md §7.7.2-3) |
| INVARIANT_VIOLATION | INVARIANT | 状態機械・所有権・ゼロサム等の不変条件違反(バグ)。IMPOSSIBLE_TRANSITION / OWNERSHIP_VIOLATION の上位分類 |
| ZC_SIGNING_NOT_CONFIGURED | INTERNAL | ZC egress 署名鍵が未設定(src/shared/zc_signature.ts) |
| ZC_SIGNATURE_WRONG_OWNER | AUTH | ZC egress 署名の検証鍵の owner_type が ZC でない |
| FX_NO_ROUTE | CONFLICT | 指定通貨ペアに ACTIVE な見積経路が無い(POST /api/fx/quote・/api/fx/transfers) |
| FX_QUOTE_EXPIRED | CONFLICT | 経路上の見積が失効(valid_to 超過・WITHDRAWN) |
| FX_ALREADY_REFUNDED | CONFLICT | 払戻済みの FX 送金への claim(POST /api/fx/transfers/:gtid/claim)。かつて FX_QUOTE_EXPIRED を流用していたが、「見積が切れた」と「もう払い戻した」は呼び出し側の次の行動が異なるため分離した |
| FX_CLAIM_WINDOW_EXPIRED | CONFLICT | claim 時点で最上流 timelock までの残余が 1 ホップ分(FX_HTLC_HOP_MARGIN_MS)を切っており、決済を開始しても間に合わない。ゲートを取らずに払戻側へ倒す(POST /api/fx/transfers/:gtid/claim) |
| FX_RATE_MISMATCH | VALIDATION | 再プライシング後の effective_rate が min_effective_rate より不利(POST /api/fx/transfers) |
| INVALID_FX_RATE | VALIDATION | PUT /api/fx/rates のレート値・通貨・有効期限が不正 |
| FX_FXP_ACCOUNT_MISSING | VALIDATION | fxp_accounts に経路上の全 FXP×全通貨のキーが揃っていない |
| FX_ROUTE_INCONSISTENT | VALIDATION | 予約コード(経路の連結性違反)。buildFxEdges が構築時に連結を保証するため現在どこからも投げられない(20_method_design.md §17.5-3) |
| FX_LIQUIDITY_INSUFFICIENT | CONFLICT | 予約コード(FXP の流動性不足)。GTID レーンの H 予約失敗パスが汎用的に検出するため現在どこからも投げられない(20_method_design.md §17.5) |
DomainError を経由しない reason_code(直接 HTTP 返却)
上表は REASON_CODE_CATEGORY(src/shared/errors.ts)に登録され、categoryOf() →httpStatusOf() の写像を持つコードである。これとは別に、入力境界で jsonError(status, code, …)
により直接 HTTP を返すコードが存在する。両者を混同しないため、代表的なものを以下に挙げる。
| reason_code | HTTP | 発生箇所 |
|---|---|---|
| USE_HTLC_ENDPOINT | 422 | POST /api/transfers に lane=HTLC(§ POST /api/transfers。副作用の前に拒否) |
| DAILY_LIMIT_EXCEEDED | 422 | Participants.daily_amount_limit 超過(アトミック UPDATE の meta.changes=0) |
| AMOUNT_EXCEEDS_TX_LIMIT | 422 | Participants.tx_amount_limit 超過 |
| PARTICIPATION_MODE_RECEIVE_ONLY | 422 | RECEIVE_ONLY 参加行が送金を起票 |
| AMOUNT_BALANCE_MISMATCH | 422 | GTID の通貨別金額均衡違反(advanceGtid。10_requirements.md §3.2.4-7) |
| MISSING_LEG_ROLE | 422 | GTID に PAYER / PAYEE いずれかの leg が存在しない(同上) |
| OVER_REVERSAL | 422 | Reversal 累計が元 TX 金額を超過(POST /api/reversals) |
| APPROVAL_REF_REQUIRED | 422 | 事前承認必須 reason に approval_ref が無い(10_requirements.md §4.3.1) |
| INVALID_MANDATE_ID | 400 | mandate_id が MANDATE- で始まらない |
| PURPOSE_CODE_REQUIRED | 403 | 照会に X-Purpose-Code が無い(§照会の認可) |
| REQUESTER_UNIDENTIFIED | 403 | 照会の主体が識別できない(同上) |
| PARTICIPANT_SIGNATURE_REQUIRED | 403 | 鍵登録済みの参加行が署名なしで照会した(同上) |
| PARTICIPANT_SIGNATURE_INVALID | 403 | 署名不正/他行の鍵/リプレイ(同上) |
| CROSS_PARTICIPANT_SCOPE | 403 | 参加者が一覧・フィード系を照会した(同上) |
| FATF_DATA_REQUIRED / FATF_VALIDATION_FAILED | 400 | クロスボーダーの FATF R.16 データ不備 |
規範(category の決まり方):
jsonError()は、まずcategoryOf(reason_code)を引く。登録済みならその category を用いる。未登録の場合に限り、HTTP ステータスから category を
導出する(400/422→VALIDATION、401/403→AUTH、404→NOT_FOUND、409→CONFLICT、429→
RATE_LIMIT、502→DOWNSTREAM、504→TIMEOUT)。したがって直接 HTTP 返却のコードは未登録でも安全に分類される。
ただし
DomainErrorは例外である:throw new DomainError(code, …)のcodeが未登録だとHTTP ステータスの手がかりが無く、
categoryOf()はINTERNAL(500・retry 不可)へ落ちる。
DomainErrorに渡すreason_codeは必ずREASON_CODE_CATEGORYへ登録すること。この不変条件は
test/invariants/spec_refs.test.tsが機械検査する。
規範(追加時の手順):新規 reason_code を追加する場合は src/shared/errors.ts のREASON_CODE_CATEGORY と本表を同じ変更で更新する。両者の一致はtest/invariants/spec_refs.test.ts が突合し、片方だけの更新は CI で落ちる。
照会の認可(横断仕様)
要件 S-5(アクセスは目的コードなしに成立しない)と S-7(参加行間の越境参照が構造的に
不可能)は、ひとつの機構として実装する(10_requirements.md §8.5、実装src/zc/platform/access.ts、対象経路表 src/zc/platform/access_routes.ts)。両者は同じ 2 つの
事実——誰が訊いているかと何を見てよいか——を必要とするため、分けると主体解決が 2 つ生まれて
やがて食い違う。
リクエストヘッダ
X-Purpose-Code: P01|P02|P03|P04|P05|P06|P07 # 必須(`10_requirements.md` §3.3.2.2.1)
X-Bank-Id: 001 # 参加者スコープ
X-Cron-Secret: <CRON_SECRET> # 運営スコープ(両方あれば運営が優先)
# 参加者の主体認証(当該行に ACTIVE な PARTICIPANT 鍵があるときは必須)
X-Participant-Key-Id: KEY-001 # KeyRegistry(owner_type='PARTICIPANT')
X-Participant-Sig-Nonce: string # 鍵ごとに一意(リプレイ防止)
X-Participant-Sig-Time: RFC3339 # 署名時刻(スキュー検査)
X-Participant-Signature: base64 # 下記ペイロードへの署名
参加者の主体認証(規範)
X-Bank-Id 単独は主張であって身元ではない。 誰でも他行の ID を書けるため、これだけでは
当事者判定(S-7)が飾りになる。参加者は KeyRegistry(owner_type='PARTICIPANT')の鍵で
リクエストに署名して主体を証明する——ZC egress 署名(shared/zc_signature.ts)の鏡像である。
署名対象は次の 4 項目(buildSignedMessage の payload。30_internal_design.md §12.6 と同じ正規化)。
{ "method": "GET", "path": "<資源識別子>", "bank_id": "001", "purpose_code": "P01" }
path と purpose_code を署名に含めるのは、ある照会で得た署名を別の照会へ付け替えられない
ようにするためである。含めなければ、一度許可された署名が nonce の有効な間だけ「何でも読める券」に
なる。
強制は鍵の登録状態で決まる(規範):当該参加行に ACTIVE な PARTICIPANT 鍵が 1 本でもあれば、
署名は必須(欠落は 403 PARTICIPANT_SIGNATURE_REQUIRED)。鍵が無い参加行は従来どおりX-Bank-Id の申告で読めるが、アクセス監査台帳には未認証として記録される
(subject_id に (unauthenticated) を付す)。
なぜ「ヘッダの有無」で切り替えないか:ZC egress の署名移行は
X-ZC-Key-Idの有無で新旧を両受けしている(§メッセージ・スキーマ進化)。あれは送信側の移行であり、世代を選ぶのは
送信者自身なので正しい。受信側の真正性検査を呼び出し側の任意で切れるようにしたら、
検査していないのと同じである。したがってここでは切替の権限をレジストリ側に置き、鍵を登録した参加行から順に強制が有効になる形にした(参加行ごとの段階移行であり、
一斉切替の日を作らない)。
鍵の所有者検査:署名が通っても、その鍵が owner_type='PARTICIPANT' かつowner_ref == X-Bank-Id でなければ拒否する(403 PARTICIPANT_SIGNATURE_INVALID)。
これが無いと、登録済みのアテスターや Watcher の鍵で任意の参加行になりすませる。
鍵の登録・失効は制度行為であり、4 眼承認の統制に従う(10_requirements.md §3.3.4)。
判定と応答
| 状況 | 応答 | なぜその番号か |
|---|---|---|
| 目的コードが無い/未知の値 | 403 PURPOSE_CODE_REQUIRED |
参照する前に拒否するため、応答は対象の存在を漏らさない |
| 主体が識別できない | 403 REQUESTER_UNIDENTIFIED |
同上 |
| 鍵登録済みの参加行が署名を付けない | 403 PARTICIPANT_SIGNATURE_REQUIRED |
主張した行であることを証明していない=その行ではない |
| 署名が不正/他行の鍵/リプレイ | 403 PARTICIPANT_SIGNATURE_INVALID |
同上。DataAccessViolationDetected を記録する——登録鍵に対する不正署名はなりすましの形そのもの |
| 参加者が当事者でない | 404 NOT_FOUND(存在しない場合と同一本文) |
403 は「在るがあなたのものではない」と答えてしまう。それは S-7 が禁じた越境の事実そのもの(20_method_design.md §9.4.4 (B)-1 と同じ論理) |
| 参加者が一覧・フィードを要求 | 403 CROSS_PARTICIPANT_SCOPE |
10_requirements.md §3.3.2.2.3 が参加者の照会を「取引ID/CASE ID/当事者キー」に限定している。黙って絞り込むのではなく拒否する——絞り込みの実装漏れは静かに漏れる |
| 運営スコープ | 許可(横断可) | 監督・運用の職務。すべて監査台帳に残る |
当事者の定義:取引=payer/payee 行、GTID=全レッグの行、HTLC=payer/payee 行、
受取側起点オーソリ=payer/payee 行、CASE=紐づく取引・GTID の当事者、Reversal=元取引の当事者、
Circuit Breaker(個別)=当該行自身。取引の派生ビュー(/events・/explain・/story・/verify・/reversals)は元取引と同じ判定に従う。
公開のまま残す照会:GET /api/dns/:business_date/status(全参加主体向けの公式ステータス)、/api/banks、OpenAPI 仕様書等。公開である理由を経路表に明記しており、理由の無い除外はtest/invariants/query_access.test.ts が落とす。同テストは、新しい GET /api/… が経路表にも
公開一覧にも無い場合にも落ちる——認可漏れは「書き忘れ」で起きるため、書き忘れ自体を検出する。
閉域照会は別規範:GET /api/dns/:business_date/hold_detail は拒否を一律 404 に統一する
(HOLD の有無自体が閉域情報であるため)。上表の 403/404 の使い分けは適用しない。
監査台帳:許可・拒否のいずれも AccessAuditLog に記録する(31_schema.md § AccessAuditLog)。
台帳の書込み失敗で照会を失敗させてはならない(可用性要件 A-2 との衝突を避ける)。
未充足:鍵を登録していない参加行は申告のみで読める(移行のための意図的な経路)。
系として「参加行が他行を騙れない」と言えるのは全参加行が鍵を登録し終えた時点であり、
それは制度側の移行工程(登録の督促と期限)に属する。機構・強制・監査は揃っている——
残っているのは運用(全行の鍵登録)であって設計ではない
(
30_internal_design.md第10章 Roadmap)。
状態 reason_code(横断仕様)
reason_code という語は本書群で 3 つの異なる値空間を指す(10_requirements.md 序章
§ reason_code の 3 つの値空間)。上の
§ エラーカタログ が定めるのは エラー reason_code——要求が拒否された理由——だけである。
本節は残る 2 つ、すなわち 状態 reason_code(Transactions.reason_code 列)と
CASE reason_code(Cases.reason_code 列)を扱う。
位置づけ
- 説明するもの:受理された取引が いま その状態にある理由。「なぜ止まっているのか」「なぜ取り消されたのか」。
- 載る場所:
GET /api/transactions/:txidのreason_code、FinalityLog の payload、Cases.reason_code。 - HTTP ステータスを持たない。 したがって
REASON_CODE_CATEGORYへの登録は要求しない
(登録済みの値と綴りが一致することはあるが、それは共用であって規約ではない)。 - 窓口の説明はこの空間に紐づける(
20_method_design.md§10.4.1.1)。
主要な値(レーン・局面別)
| reason_code | 付く局面 | 遷移 |
|---|---|---|
SUSPEND_NAMECHECK_PENDING |
名義確認の応答待ち | PRECHECKED → PRECHECKED_SUSPENDED |
SUSPEND_AUTHORITY_PENDING |
AML/制裁照会(Authority Check)の応答待ち。判定不能の時点で PRECHECKED のまま先置きし、T_auth 超過で遷移する(20_method_design.md §3.3.1) |
PRECHECKED → PRECHECKED_SUSPENDED |
SUSPEND_EXEC_TIMEOUT |
Decision 後、a が期限内に成立しない | DECIDED_TO_SETTLE → SUSPENDED |
SUSPEND_PAYEE_PROOF_TIMEOUT |
a 成立後、b が期限内に成立しない | PAYER_EXEC_CONFIRMED → SUSPENDED |
FAILED_EXEC_TIMEOUT |
SUSPENDED の滞留が上限を超えた |
SUSPENDED → FAILED_EXECUTION |
IGS_FAILED |
中銀決済が HOLD または不成立で返った(両者を区別しない。下記の注意) | PAYER_EXEC_CONFIRMED → SUSPENDED |
BOJ_INSUFFICIENT_FUNDS |
HIGH_VALUE 受付時に中銀当座残高が不足 | PRECHECKED → DECIDED_CANCEL |
DNS_HOLD_IGS_STOPPED |
DNS_HOLD 中で igs_mode=STOP(全件停止) |
PRECHECKED → PRECHECKED_SUSPENDED |
DNS_RINGFENCED |
igs_mode=RINGFENCED で原因行として隔離 |
同上 |
DNS_IGS_THROTTLED |
igs_mode=RINGFENCED_PLUS で公平性予算を超過(Defer) |
同上 |
INSUFFICIENT_FUNDS |
参加行側の残高不足 | PRECHECKED → DECIDED_CANCEL |
CANCEL_BY_PAYER |
支払人による取消 | → DECIDED_CANCEL |
TIMELOCK_EXPIRED |
HTLC の timelock 到来 | HTLC_LOCKED → DECIDED_CANCEL |
RECHECK_AUTHORITY_NG |
claim 直前の AML 再照会が NG | HTLC_LOCKED → DECIDED_CANCEL |
RECHECK_AUTHORITY_UNAVAILABLE |
claim 直前の AML 再照会に答えが返らない(回路 OPEN 等)。claim を拒否するが取消はせず、再試行に委ねる(30_internal_design.md §15.4) |
遷移なし(証跡のみ) |
INVALID_PREIMAGE |
preimage 不一致(状態は維持し、証跡のみ残す) | 遷移なし |
MISRECORD_CORRECTED |
誤記録訂正(10_requirements.md §4.4) |
追記のみ |
SUSPEND_ADAPTER_DOWN |
参加行 Adapter へ到達できない(Circuit Breaker が OPEN)。「相手が拒否した」EXEC_*_FAILED とは別値——CASE 集約(20_method_design.md §10.9.3.6)がこの区別に依存する |
→ SUSPENDED |
EXEC_DEBIT_FAILED / EXEC_CREDIT_FAILED |
参加行に届いたうえで実行が失敗した | → SUSPENDED |
CREDIT_FAILED_PROOF_REQUIRED |
Reversal 起票に物理的不能の証明が無く CASE へ収束(10_requirements.md §4.3.0) |
CASE 起票 |
注意(
IGS_FAILEDは HOLD と不成立を区別しない):中銀決済がHOLD(一時的な流動性不足。再試行で解消しうる)で返った場合も、
FAILED(不成立)で返った場合も、取引に付く状態
reason_codeは 同一のIGS_FAILEDである。両者を分けるのはIgsRequests.statusであって取引側の
reason_codeではない。したがって 窓口が「待てば済むのか、済まないのか」をreason_codeだけで判断してはならない——判断材料は照会応答のexternal_settlement(
{status, retriable})である(20_method_design.md§9.4.4.1 (A))。
命名規約(規範)
状態 reason_code は 2 つの family に分かれる。どちらの family かで綴りを決める。
| family | いつ使うか | 綴り | 例 |
|---|---|---|---|
| 待機理由 | 何かを待っている——時間経過または相手の応答で解ける。窓口の答えは「待てば進む」 | SUSPEND_*。待ちが解けずに終わったときは CANCEL_*_TIMEOUT(=解けなかった待ちの帰結を接頭辞にする) |
SUSPEND_NAMECHECK_PENDING・SUSPEND_EXEC_TIMEOUT・SUSPEND_ADAPTER_DOWN・CANCEL_PRECHECK_TIMEOUT |
| 事象理由 | 確定した事実が起きた——待っても変わらない。窓口の答えは「この理由で止まった/終わった」 | 事象そのものの名前(接頭辞を付けない) | IGS_FAILED・BOJ_INSUFFICIENT_FUNDS・DNS_RINGFENCED・TIMELOCK_EXPIRED |
規範
- 遷移先を名前に含めない(事象理由)/含める(待機理由)という上表の使い分けを守る。
遷移先そのものはstate列が既に持っているので、事象理由にまで接頭辞を付けると重複になり、CANCELLEDの行にSUSPEND_*が載るような矛盾する組み合わせを作れてしまう。CANCEL_で始まる値がすべて待機理由なのではない。 待機理由に属するのは
解けなかった待ちの帰結、すなわちCANCEL_*_TIMEOUTの形だけである
(CANCEL_PRECHECK_TIMEOUT)。一方CANCEL_BY_PAYERは支払人が取り消したという確定した
事実であり、待ちではない——待機理由の綴りを共有しているが事象理由に属する。
接頭辞ではなく「待っていたのか、起きたのか」で family を決める、が上表の趣旨である。
- エラー
reason_codeを状態reason_codeとして転用しない。 両空間は別物である
(10_requirements.md序章)。転用すると、窓口が見る値に「API 呼び出しが失敗した理由」が
混入する。かつて Adapter 不通がCIRCUIT_OPEN(エラー空間の値)で表現されずEXEC_DEBIT_FAILEDに潰れていたのは、この線引きが無かったためである
(現在は待機理由SUSPEND_ADAPTER_DOWNを用いる)。 - 「相手が拒否した」と「相手に届かなかった」を同じ値にしない。 前者は事象理由、
後者は待機理由であり、顧客への説明も運用の打ち手も異なる。
規範
- 本表は網羅ではない。 状態
reason_codeは局面ごとに追加され得るため、本表は窓口・運用が
分岐に使う主要値を固定する。網羅的な現行値は実装(Transactions.reason_codeに書き込む
全箇所)を正とする。 - 本書群が規範として名指しする状態
reason_codeは、実在する値でなければならない。
規範として先に固定したが未実装の値は、【未実装】を付して明示する(30_internal_design.md§10.0)。
この不変条件はtest/invariants/spec_refs.test.tsが機械検査する。 - 遷移の条件を状態
reason_codeで書かない。reason_codeは人間向けの説明ラベルであり、
状態機械のガードではない。遷移を縛るのは状態そのもの(ALLOWED_TRANSITIONS)と、external_settlement_statusのような専用の判定列である(20_method_design.md§3.2.1)。