ブロックチェーンのreorgとファイナリティ|入金は何ブロック待てばよいか

コラム

/約15分で読めます

コラム

/約15分

ブロックチェーンのreorgとファイナリティ|入金は何ブロック待てばよいか
目次(タップで折りたたみ)

    あるサービスでは、利用者がチェーン上で入金すると、そのイベントを見てすぐに残高を増やしていました。利用者はその残高で、すぐに出金もできます。ある日、入金が入っていたブロックが、チェーンの再編(reorg)で正規のチェーンから外れました。入金はなかったことになり、出金だけが残ります。サービスは、同じお金を2重に失いました。

    チェーン上のイベントは、受け取っただけでは業務の確定に使えません。ブロックハッシュと確定の状態を記録し、チェーンに合った条件を満たしてから、出金や権利の付与のような外部の処理を実行します。

    では、何ブロック待てばよいのか。実は、「確定」が保証している中身は、チェーンごとに違います。

    • Ethereumのsafe(通常は覆りにくい)とfinalized(覆すには大きな罰がある)
    • Bitcoinの確認数
    • ロールアップ(Rollup)の「シーケンサが受け付けた」「L1に載った」「L1で確定した」

    だから、「何ブロック待てば絶対に安全か」という共通の答えはありません。アプリ側では、この違いを踏まえて次の4つを決めます。

    1. イベントを、どんな段階(状態)に分けて管理するか
    2. チェーンと処理ごとに、どこまで待てば確定とみなすか(確定条件の方針、finality policy)
    3. 出金や発送など後戻りできない処理を、どの段階で実行してよいか
    4. 再編で取引が消えたとき、どう埋め合わせるか

    例はEVM系を中心にします。ロールアップの仕組みの基礎はレイヤー2・ロールアップチェーン開発の解説をご覧ください。

    最初に決めるのは「どの処理が巻き戻せないか」

    同じイベントでも、「処理中」と画面に出すだけなら、確定の度合いは低くてかまいません。後で表示を戻せるからです。一方、商品の発送や別チェーンへの出金は、元のイベントが消えても自動では戻りません。

    つまり最終確定性(finality)は、画面の待ち時間のことではありません。業務上の処理を実行してよい条件のことです。まず、イベントを受けて行う処理を3段階に分けます。

    処理例実行条件の考え方
    可逆な表示着金検知、取引履歴、進捗表示最新ブロックを使えるが、未確定表示と自動巻き戻しが必要
    内部で補償可能ポイント仮付与、予約枠、社内通知safe等を候補にし、金額・不正の余地・補償費用で決める
    外部・不可逆出金、商品発送、証明書発行、別台帳の確定原則としてプロトコル上のfinalized相当を待ち、実行前に正規チェーン上にあるかを再確認

    再編が起きたときの影響は、段階ごとに違います。

    • 可逆な表示:表示を「未確定」へ戻せる
    • 内部で補償可能:取り消しの処理と監査の記録が要る
    • 外部・不可逆:元のイベントだけが戻り、2重の損失になりうる

    待ち時間を短くしたいからといって確定の条件を弱めると、冒頭のような2重の損失になりえます。そこで条件は弱めず、先に行う処理を、元に戻せる形にします。

    冒頭のサービスなら、入金を見たら残高にすぐ足さず、「まだ使えない仮の残高」として見せます。確定したら、使える残高へ移します。速さの要求と、許せる損失を同じ数字に押し込まないことです。画面の反応と、業務の確定を分けて作ります。

    アプリの状態を「検知・安全・確定・反映済み」の4段階に分ける

    トランザクションのsuccessは、実行が成功したことを示すだけです。そのブロックが、これからも正規のチェーンに残るという保証ではありません。

    アプリのDBには、少なくとも次の状態を持たせます。

    • 送信済み(SUBMITTED):トランザクションを送った
    • 検知(OBSERVED):処理結果(receipt)やイベントを見つけた
    • 安全(SAFE):safeの条件を満たした
    • 確定(FINALIZED):finalizedの条件を満たした
    • 反映済み(APPLIED):冪等キーを付けて、外部の処理を実行した
    • 外れた(ORPHANED):検知か安全の段階で、再編により正規チェーンから外れた

    冪等キーは、同じ依頼が何度届いても1回分しか処理しないための識別子です。状態の移り方の図は、後半の「実装する人向けの詳細」に載せています。

    「外れた」は、消すという意味ではありません。「前は見えていたが、正規チェーンから外れた」ことを、監査できる形で残す状態です。新しい正規チェーンに、同じトランザクションが入り直すこともあります。だから、業務のIDと、チェーン上のどこに入ったかを同じものとして扱いません。

    保存する最小の項目は次のとおりです。

    • chain_id、トランザクションハッシュ、ブロック番号、ブロックハッシュ
    • ログ番号、コントラクトアドレス、イベントの種類
    • 見つけた時刻、いまの確定状態

    イベントの購読には落とし穴があります。Geth公式ドキュメントによると、再編の前に送られたログはremoved: true付きで送り直されます。同じトランザクションのログが何度も届くこともあります(Geth: Real-time Events)。WebSocketの通知が順番どおり、1回だけ届くとは考えません。一定の範囲を定期的に取り直しても、つじつまが合うようにしておきます。

    再編は、ブロック番号ではなくハッシュのつながりで見つける

    ブロック高100のデータを保存した後、同じ高さ100に別のブロックが正規として採用されることがあります。だから、処理した位置をlast_processed_block = 100だけで持つ作りでは足りません。

    処理したブロックごとに、自分のハッシュ(hash)と親のハッシュ(parentHash)を保存します。新しいブロックの親が、直前に保存したハッシュと一致するかを確かめます。

    1. 食い違いに気づく:新しいブロックのparentHashと、保存済みの先頭のブロックのhashを比べます。
    2. 共通の祖先を探す:RPCと保存済みのヘッダを後ろへたどって照らし合わせ、正規チェーンと最後に一致するブロックを見つけます。
    3. 影響を切り離す:祖先より後のイベントを「外れた」にします。そこから作ったまだ確定していないデータも、同じDBトランザクションの中で戻します。
    4. 新しい枝を処理し直す:祖先の次のブロックから、ふだんの取り込みをやり直し、確定の状態を評価し直します。

    イベントからは、残高、注文、通知の予定、集計、処理した位置などが作られています。古いログを消すだけでは、これらが食い違ったまま残ります。だから再編の処理では、イベントから作った、まだ確定していないものすべてを、同じ時点まで戻します。

    データの取得・保存・作り直しは、インデクサ側の課題です。ここで扱うのは、そのデータを受け取るアプリが「もう確定した」と判断するところです。

    1つの判断に使う照会は、同じブロックにそろえる

    1つの業務の判断のために、残高、持ち主、許可の状態を順番にRPCで照会するとします。途中で再編が起きると、違うチェーンの状態を混ぜて判断してしまうかもしれません。

    そこで、呼び出しのたびにlatest(最新)を指定するのはやめます。先に対象のブロックを決め、すべての読み取りを同じブロックハッシュにそろえます。

    EIP-1898は、そのための方法を定めています。ブロックハッシュを指定し、requireCanonical(正規チェーン上のブロックに限る)を付けることで、再編中でも一貫した状態を照会できます。

    ただし、EIP-1898がすべてのRPCで使えるとは限りません。使うRPCやSDKが、そのメソッドでブロックハッシュの指定に対応していない場合もあります。そのときは同じブロック番号で各照会を行い、処理の前後でその番号のハッシュが変わっていないかを確かめ直します。照会の書き方の例は後半にあります。

    確定の条件を、チェーン・処理・金額ごとに決める

    チェーンの種類によって、アプリが区別する段階そのものが次のように違います。

    チェーン種別アプリが区別する段階実装上の注意
    Ethereum PoSlatest / safe / finalizedブロック数の推測よりRPCのブロックタグを使う。確定が遅延・停止する場合も、時間切れを理由に成功扱いにしない
    Bitcoin型PoW未収録 / 収録 / 確認数の増加確率的な確定性。金額と攻撃・二重支払いのリスクに応じて必要確認数を決める
    OP Stack系Rollup送信済み(pending)/ シーケンサ承認(unsafe)/ L1公開(safe)/ L1確定(finalized)シーケンサの応答をL1確定と同一視しない。L2上の取引とL1への出金(withdrawal)も別の方針にする

    段階が違う以上、「全チェーンで12確認」のような1つの値は使えません。確定条件の方針は、少なくともチェーン×処理の種類×リスクの区分で管理し、設定の変更を監査できるようにします。

    それぞれの公式資料も、段階を分けて説明しています。

    • Ethereum:JSON-RPCで、safeを最新の安全な先頭、finalizedを最新の確定済みブロックとして参照できる(ethereum.org: JSON-RPC API)
    • Bitcoin:公式Developer Guideは、送信しただけでは支払いを保証しないとしている。ブロックが積み重なるほど、履歴の置き換えから守られる(Bitcoin: Payment Processing)
    • OP Mainnet:シーケンサが収録した後、Ethereumに公開した後、Ethereumで確定した後を、別の状態として示している(Optimism: Transaction statuses)

    OP Mainnetの確定性の解説では、時間の目安を次のように分けています(Optimism: Transaction finality)。

    • シーケンサによる事前の確認:数百ミリ秒単位
    • L2のブロック:約2秒
    • Ethereumでの確定まで:15〜30分程度
    • 7日間の待機:標準ブリッジの出金だけに当てはまる

    確定は、遅れたり止まったりすることがあります。だから、時間だけを条件にはしません。「送ってから15分経った」ではなく、「対象のブロックがいまの正規チェーン上にあり、確定済みの先頭より前に入った」ことを判定します。

    確定済みの先頭が進まないときは、待つ、機能を絞って動かす、人が確かめる、のどれかに移ります。期限が来たことを、確定の代わりにしません。

    「確定した」と「やることリストに入れた」を同時に記録し、受け取る側でも2重実行を防ぐ

    DBの更新と外部APIの呼び出しを、1つのトランザクションにまとめることは、ふつうできません。そこでアウトボックス(outbox)を使います。DBに書く「これから外部に頼む仕事の一覧」のことです。

    「確定」への更新と、アウトボックスへの登録を、同じDBトランザクションで行います。外部の処理は、アウトボックスを処理するワーカーが実行します。確定への更新が成功した行からだけ、仕事を登録します。対象の行やブロックハッシュが一致しなければ、外部の処理は登録しません。書き方の例は後半にあります。

    アウトボックスの仕事が、2回届くこともあります。受け取る側でも同じ冪等キーを記録し、前の処理結果を使い回します。受け取る側に冪等性や結果を照会する手段がないなら、1回だけの実行は保証できません。返事が分からないときは、自動で送り直すのを保留します。

    ワーカーは外部のAPIにも同じ冪等キーを送ります。タイムアウトした後は、相手側の結果を照会してからやり直します。

    冪等キーをtx_hashだけにすると、同じ業務の操作が送り直されたり置き換えられたりしたときに、2重の処理を防げないことがあります。注文IDや入金IDなど業務上の一意のIDに、処理の種類と版番号を組み合わせます。

    確定する前に処理を許すなら、埋め合わせの処理を「後で考える」のではいけません。ふだんの処理と一緒に決めておきます。埋め合わせのできない商品の発送や、ほかのネットワークへの送金に、ポイントの取り消しと同じ作りは使えません。

    再編が起きたとき、利用者に何を見せ、運用で何をするか

    利用者の画面では、「成功」だけでなく、「検知済み」「確定待ち」「確定済み」「再確認中」を分けて見せます。

    再編で消えたイベントが、新しい枝に入り直すこともあります。消えた直後に「失敗」と言い切ると、入り直したときに表示と食い違います。だから、まず「再確認中」に移します。そのうえで、正規チェーン上に入り直したか、nonce(送信元ごとの通し番号)、置き換えのトランザクション、業務IDを照らし合わせて、最後の状態を決めます。

    運用では、次のものを見張ります。

    • チェーン別のlatest - safe、safe - finalizedの遅れと、確定済みの先頭が止まっている時間
    • RPCの間で、同じブロック高のハッシュが食い違っていないか。再編の回数と深さ
    • 「検知」「安全」のまま止まっているイベントの数と、最大の経過時間
    • 「外れた」イベントから、外部の処理が実行されていないか
    • アウトボックスのやり直し、重複の拒否、人による埋め合わせ待ちの件数

    次のことに気づいたら、自動で確定へ進める処理と、後戻りできない処理を止めます。

    • 想定より深い再編、複数のRPCの食い違いが続く
    • 確定が止まった
    • finalized相当として扱ったイベントが消えた

    すでに外部の処理を実行した取引は、自動で消しません。対象の業務ID、金額、外部の処理ID、旧・新のブロックハッシュをまとめて、1件ずつの対応として起票します。計画されたフォークのときの入出庫の停止と再開の基準は、ハードフォーク時のリプレイ防止と入出庫の再開手順も参照してください。

    本番の前に、再編と確定の停止をわざと起こして試す

    トランザクションが正常に確定する試験だけでは、設計の要を確かめられません。ローカルの開発用ネットワークやテスト用ノードで、次の場面を自動で試します。

    1. イベントを取り込んだ後に別の枝を正規にし、古いイベントが「外れた」になって、そこから作った状態が戻ること。
    2. 同じログ、removed: trueのログ、新しい枝のログを、重複・順番の入れ替えで渡しても、最後の結果が一致すること。
    3. 確定済みの先頭を止め、時間が経っても、後戻りできないアウトボックスの仕事が作られないこと。
    4. DBのコミット直後や、外部APIの返事の直前にワーカーを止め、再起動しても外部の処理が1回だけ実行されること。
    5. 2つのRPCに違う先頭のブロックを返させ、食い違いで確定への移行が止まり、警報が鳴ること。
    6. 再編の後、同じ業務の操作が別のトランザクションに入り直しても、業務IDで見て2重に付与しないこと。

    合格の条件は、「エラーにならない」ではありません。アプリのDB、外部のシステム、利用者の表示、監査ログが、期待した状態にそろうことを確かめます。

    試験で、埋め合わせのできない処理が見つかることもあります。そのときは、待つ確定の度合いを強めるか、業務の流れそのものを元に戻せる段階に分けます。

    実装前の設計チェック

    • 各処理を、可逆・補償可能・不可逆に分け、責任者を決めた。
    • チェーン・処理・リスク区分別の確定条件の方針と、変更の履歴がある。
    • ブロック番号、ハッシュ、parentHash、ログの位置、確定状態を保存する。
    • 同じ判断に使う複数のRPC照会を、同じブロックハッシュにそろえる。
    • 「外れた」と入り直しを状態の変化として扱い、物理的な削除に頼らない。
    • 確定への更新とアウトボックスへの登録を、同じDBトランザクションで行う。
    • 外部の処理に、業務IDにもとづく冪等キーと、結果を照会する手段がある。
    • 確定の遅れ・RPCの食い違い・深い再編のときの停止条件と、運用手順書がある。
    • 再編、通知の順番の入れ替え、ワーカーの停止を含む障害の試験を通した。

    実装する人向けの詳細

    ここからは、状態管理と照会・アウトボックスを実際に書く開発者向けのコード例です。

    状態の移り方

    SUBMITTED
       │ receipt / eventを検知
       ▼
    OBSERVED ── reorg ──> ORPHANED
       │ safe条件を満たす
       ▼
    SAFE ────── reorg ──> ORPHANED
       │ finalized条件を満たす
       ▼
    FINALIZED
       │ 冪等キーで副作用を実行
       ▼
    APPLIED
    

    照会を同じブロックハッシュにそろえる

    先に確定済みのブロックを取り、その同じハッシュで残高・権限・コントラクトの状態を読みます。

    // 概念例: 先にfinalized blockを取得する
    block = eth_getBlockByNumber("finalized", false)
    
    // 同じblock hashに対して残高・権限・contract stateを読む
    blockRef = { blockHash: block.hash, requireCanonical: true }
    balance   = eth_getBalance(account, blockRef)
    owner     = eth_call(ownerQuery, blockRef)
    allowed   = eth_call(permissionQuery, blockRef)
    

    確定方針の管理単位

    確定条件の方針は、chain_id × action_type × risk_tierの単位で持ちます。

    確定への更新とアウトボックスの登録

    「確定にした」と「やることリストに入れた」を、同じ書き込みで行う例です。

    -- PostgreSQLの概念例。事前に対象ブロックの正規性と確定条件を検証する
    BEGIN;
      WITH promoted AS (
        UPDATE chain_events
           SET finality_status = 'FINALIZED'
         WHERE event_id = :event_id
           AND block_hash = :verified_canonical_hash
           AND finality_status IN ('OBSERVED', 'SAFE')
        RETURNING event_id
      )
      INSERT INTO outbox_jobs (idempotency_key, action, payload)
      SELECT :business_id || ':grant:v1', 'GRANT_ENTITLEMENT', :payload
        FROM promoted
      ON CONFLICT (idempotency_key) DO NOTHING;
    COMMIT;

    関連記事

    XTELAができること

    私たちは、対象チェーンの確定性の調査から、オンチェーンイベントと業務状態の対応づけ、インデクサ・バックエンド・アウトボックスの設計と開発までを一続きで進めます。再編やRPC不一致を再現する障害試験、監視と復旧手順書の整備、小さく検証するPoCも行います。技術上の確定性と業務上の確定を分けた実装へ整理したい場合は、お問い合わせフォームからご相談ください。

    主要参考資料

    資料の確認日と注意

    2026年8月〜9月24日に確認した公開一次資料にもとづく、一般的な技術設計の解説です(OP Mainnetの時間の目安は2026年9月24日時点)。プロトコル、ノードソフトウェア、RPC、Rollupの仕様は変わりえます。実装する時点で対象チェーンの公式資料を確認してください。資産の取扱いや会計・法的な判断は、各分野の専門家と進めてください。

    お問い合わせ

    どんなフェーズからでも、お持ちのアイデアや企画をもとにご提案可能です。
    まずはお気軽にご相談下さい!