Crypto OS
Technical Crypto OS第5段階 · インフラとセキュリティ

T25 · Blockchain Data Infrastructure

チェーン全体のデータを、検索できるウェアハウスにするには?

この章を 3D で体験する →
練習する能力
BuilderOnchain Literacy
実践
プロトコルの過去のイベントをすべて列指向ファイルに保存し、複数月にまたがる集計クエリを実行する。
AI Lab
AI に分析クエリを書かせ、その集計定義がオンチェーンの生イベントと一致するか自分で確認する。

現実の問題

T17 の索引器は安定しています。ブロックを取りこぼさず、ロールバックも再実行もできます。業務DBには数千万件の送金イベントがあります。

運用担当から最初の質問が届きます。「過去12か月の月間アクティブアドレス数と手数料収入はいくらですか?」

SQLを実行すると、40分後に結果が出ました。同時に本番の警報が鳴ります。検索がDBのIOを使い切り、フロントエンドのAPIがタイムアウトしています。

索引を追加すると少し速くなりました。しかし次は、「新規ユーザーと既存ユーザーに分けてください」。また30分かかります。

三つ目は、「24か月分を見られますか?」

DBには直近6か月しかありません。ディスクの増加が速すぎて、3か月前に古いデータを削除する定期処理を作ったからです。

どれもごく普通の質問です。運用側には数回クリックすれば出るように思えます。それでも、システムは一つも十分に答えられません。

索引器の実装が悪いわけではありません。一つのアドレスの現在残高を調べるシステムと、10億行を走査して合計するシステムは、保存形式から再実行方針まで別物です。

この章では後者を作ります。

思考実験

紙の帳簿を考えます。

一ページに一件、日付、相手、金額、用途、担当者、請求書番号、部署、案件番号など30項目を記録します。

質問一:「田中さんの昨日の支払いはいくら?」 該当ページを開けば終わります。一ページを見る負担は小さいものです。

質問二:「昨年の支出合計を月別に」 全ページをめくります。必要なのは日付と金額だけですが、ページ単位で綴じてあるので、その二列だけを見ることはできません。30項目すべてが目を通ります。

綴じ方を変えます。日付を一巻、金額を別の一巻というように、30項目を30巻に分けます。 質問二では二巻だけ読めばよく、負担は15分の1です。同じ種類の値が並ぶため圧縮率も高く、運ぶ重さまで減ります。

代わりに質問一は難しくなります。一件の全情報を得るには30巻から一つずつ取り出して組み直します。

これが行指向と列指向の違いです。優劣ではなく、得意な質問が違います。

本当に難しいのは、次の思考実験です。

帳簿を一度で作るのではなく、毎日追加し、ときには先月の一日分を丸ごと置き換えるとします。チェーン再編、解析の誤り、アップグレードによるイベント意味の変更が理由です。

問題は読む速さではなく、次になります。

ある一日を作り直した結果を、初日から全量を作り直した結果と完全に一致させるには?

これができて初めて、照合、報告、決済に使える倉庫です。できなければ、少し速いキャッシュにすぎません。

あなたが決める

三つの質問に答える方法を選びます。

結果を観察する

四つを比較します。

提供速度集計性能履歴定義の管理長期費用
業務DBの索引・ビュー最速低いディスクに制限完全に管理中、本番へ影響
読み取りレプリカ速い低いディスクに制限完全に管理中、全量複製
列指向ファイルと分析遅い高いほぼ無制限完全に管理低いが運用が必要
外部サービス最速高い深い管理できない従量課金、外部依存

四列目は事故時に他の四列以上に重要です。

分析の数字には、二つの質問が必ずあります。

どう計算した数字ですか?

再計算しても同じですか?

前者は定義、後者は再現性です。どちらも検索ではなくETLの問題です。

SQLそのものは半日で学べます。難しいのは由来を説明し、各区画を独立して作り直し、毎回同じ結果を得ることです。

半分はすでに学びました。T17 のcheckpoint、冪等性、区間表、再編ロールバックが別の形で戻ってきます。

モデルを作る

三層:Raw、Decoded、Curated

取得と指標計算を一つのコードにまとめず、三層に分けます。

  1. RPC / ノード
  2. Raw 原始層
  3. Decoded 解読層
  4. Curated 業務層
  5. 報告とAPI
各層は一つの仕事だけを行い、独立して再実行できます
層保存内容再実行の費用
Rawヘッダー、取引、レシート、ログの原始バイト。解読しないRPCの再取得。最も高く、archiveが必要な場合も
DecodedABIで構造化したイベント:誰から誰へ、いくらローカルRawの再解読で安い
Curated日次利用、手数料、残高、LP収益など集計だけで最も安い

最も高い取得を一回だけにすることが分離の価値です。

フィールド解読やアップグレード後の意味が誤っていた場合、RPCから直接指標を作ると全履歴を数日かけて再取得します。Rawがあれば数十分の再解読で済みます。

Rawは将来の解読誤りに備える保険です。 保存費用は全量再取得よりはるかに低いものです。

分割キーが決めるもの

物理ファイルを分け、関係する区画だけ読むのがpartition pruningです。

候補はブロック高と時間です。

両方使いますが役割を分けます。

物理分割キー → 日付(ブロック時刻からUTCで導出)
区画内の順序 → ブロック高、次にlog_index

業務の質問は月や四半期なので日付で切ります。書き込み、重複排除、再実行の単位はブロック高。区画内を高さ順にすると走査を省け、対応区間も正確に調べられます。

ディレクトリ例:

warehouse/
  raw_logs/
    dt=2024-03-01/part-0000.parquet
    dt=2024-03-02/part-0000.parquet
  decoded_transfers/
    dt=2024-03-01/part-0000.parquet
  curated_daily_metrics/
    dt=2024-03-01/part-0000.parquet

間違えやすい点は二つです。

第一、キーはブロック時刻から取り、now()は使いません。 実行日で分けると再実行が別区画に入り、再現性が失われます。T16 と同じく、時刻はサーバーではなくヘッダー由来です。

第二、細分化しすぎません。 一日で数十~数百MB程度が扱いやすく、時間単位では多数の小ファイルのメタデータ負担が利点を消します。

冪等性:区画単位の原子的書き込み

T17 のDB取引に対し、ファイルでは原子的な名前変更を使います。

1. 区画全体を一時ディレクトリへ書く
   warehouse/decoded_transfers/_tmp_2024-03-01_run42/
2. 書き込みと検証後、正式名へ原子的に変更
   → warehouse/decoded_transfers/dt=2024-03-01/
3. manifestへ高さ区間、行数、checksum、decoder版を記録

同じルールです。

区画は完全に存在するか、全く存在しないか。半分だけの区画を許しません。

第一段階で落ちれば一時領域を掃除し、正式領域は変わりません。名前変更後に落ちればmanifestが未更新なので再実行します。同じバイトを生成するため無害です。

名前変更が先、manifest更新が後です。 逆にすると記録だけあってファイルがない状態になり、checkpointを先に保存する誤りが再発します。

Manifest:工程の真実

manifestがなければファイルの山でしかなく、月の完全性に答えられません。

create table warehouse_manifest (
  dataset        text    not null,      -- raw_logs / decoded_transfers / ...
  chain_id       integer not null,
  dt             date    not null,      -- 分割キー
  from_block     bigint  not null,      -- 区画が実際に含む高さ区間
  to_block       bigint  not null,
  row_count      bigint  not null,
  checksum       text    not null,      -- 再実行比較用の内容指紋
  decoder_version text   not null,      -- 解読コード版。由来の鍵
  status         text    not null,      -- pending / running / done / failed
  attempts       integer not null default 0,
  built_at       timestamptz not null default now(),
  primary key (dataset, chain_id, dt)
);

事故時に必要な四つの問いへ答えます。

-- 1. 欠落はあるか?
select dt from generate_series($from::date, $to::date, '1 day') g(dt)
 where not exists (
   select 1 from warehouse_manifest m
    where m.dataset = 'decoded_transfers' and m.chain_id = $chain
      and m.dt = g.dt and m.status = 'done'
 );

-- 2. 隣接日の高さ区間は連続しているか?
select a.dt, a.to_block, b.dt, b.from_block
  from warehouse_manifest a
  join warehouse_manifest b
    on b.dataset = a.dataset and b.chain_id = a.chain_id
   and b.dt = a.dt + 1
 where b.from_block <> a.to_block + 1;

-- 3. 古いdecoderで作った再実行対象の区画は?
select dt from warehouse_manifest
 where dataset = 'decoded_transfers' and decoder_version <> $current;

-- 4. 再実行で内容が変わったか?区画ごとにchecksumを比較

第一は T17 の区間表と同じです。完了は進捗表示ではなくSQL一つで判定できる状態です。 第三で層分離の価値が実現します。

再編:倉庫は遅れてよい

不変の列指向ファイルで T17 のチェーン先端再編を扱うには?

先端を倉庫へ入れないのが最も単純です。

業務DB(OLTP) ← 先端を追い、リアルタイムと再編を担当
        │
        │ block_numberがsafeHeight以下のものだけ移す
        ↓
倉庫(OLAP)   ← 数ブロック遅れ、保存後は不変

safeHeightはT17のoutboxと同じです。数分~数時間の遅れは設計です。

倉庫にはロールバック対象が存在しない状態にします。直近一秒の質問は業務DBと T18 のリアルタイム経路が答えます。

二つのシステム、二つの鮮度、二つの整合性保証を分けます。

過去の解読誤りは区画再生成、名前変更、manifest更新、下流をpendingとして再実行します。このためdecoder_versionを残します。

Curated:定義をコードへ

ここからは技術より定義です。

月間アクティブアドレスには六つ以上の選択があります。

- 送信者だけか、送受信の両方か?
- 失敗取引を含むか?
- コントラクトを含むか?
- aggregator経由では中継者か最終利用者か?
- 一人の五アドレスは一人か五つか?
- UTCか現地時間か?

組み合わせれば多数の定義があり、すべて月間利用と呼べます。

curated層の全表に定義の注釈を付け、SQLと同じファイルへ置きます。 別文書へ分離すると、三か月後に定義がコードと食い違うためです。

-- daily_active_addresses
-- 定義(2024-03確定):
--   1. 成功取引のログだけを集計
--   2. fromとtoの両方を一度ずつ重複排除
--   3. mint/burnの相手であるzero addressを除外
--   4. 既知コントラクトは除外せず別途印を付ける
--   5. UTC暦日
create table curated_daily_active (
  chain_id   integer not null,
  dt         date    not null,
  contract   bytea   not null,
  addresses  bigint  not null,
  tx_count   bigint  not null,
  primary key (chain_id, dt, contract)
);

同じ指標の数字が違う原因の大半は定義です。 コードへ明記すると議論は一時間から一分になります。

完全な例

解読層から月次指標を作ります。

-- 12か月の月間アクティブアドレスと手数料
-- dtのpruningで対象12か月だけを読む
with e as (
  select dt, from_addr, to_addr, amount, gas_used, gas_price
    from decoded_transfers
   where chain_id = 1
     and contract = $contract
     and dt >= date '2024-01-01'
     and dt <  date '2025-01-01'
),
addrs as (
  select date_trunc('month', dt) as m, from_addr as addr from e
  union all
  select date_trunc('month', dt) as m, to_addr   as addr from e
)
select m,
       count(distinct addr)                      as active_addresses,
       sum(gas_used * gas_price) / 1e18          as fees_native
  from addrs
  join e on date_trunc('month', e.dt) = addrs.m
 group by m
 order by m;

where dt >= ... and dt < ...が重要です。12ファイルか全ファイルかを決めます。

where date_trunc('month', dt) = ...のようにdtを関数で包むとpruningが効かず全走査になります。キーを直接条件に使えるかが速度を左右します。

何と呼ぶか

ETL抽出・変換・ロード

取得、変換、保存の工程です。チェーンではブロック取得、イベント解読、指標集計に対応します。

本番DBからSQL一つで直接計算する方法との違いは工程を設計することです。一分以上かかるSQLなら、すでにETLをしているのに工程として扱っていない可能性があります。

データウェアハウスWarehouse

取引処理より分析へ最適化した保存です。遅延を受け入れ、集計性能と履歴を得ます。

残高一点検索と10億行集計を同時に担うシステムは、両方を十分にできません。

列指向ストレージColumnar Storage

列ごとに保存し、必要な列だけ読み、同型データを圧縮します。

分析の基本です。組み込み単機エンジンはプロセス内でファイルを読み、分散列指向DBは大規模・高並行向けです。量とチームで選び、モデルは共通です。

パーティションPartitioning

キーで物理分割し、関連部分だけ読むpruningです。

日付で分割し、内部はブロック高順。検索と書き込みをそれぞれ支え、冪等書き込みの原子的単位にもなります。

冪等ロードIdempotent Load

同じデータを何度読み込んでも結果が変わりません。

一時保存、検証、原子的名前変更、manifest更新は T17 のDB取引と同じ原則です。半分残すより、やり直します。

安全高度Safe Height

これより低いブロックだけを倉庫に移し、巻き戻す必要をなくします。

遅れは利用者へ公開する契約であり、隠して消す性能問題ではありません。

データの由来Lineage

区画、高さ、decoder版、集計ロジックまで数字を追跡できることです。

最小構成はmanifestの列です。なければ数字の変化を推測するしかありません。

実践

実践履歴を列指向ファイルへ保存し、複数月を集計する任意の言語、列指向エンジン、公開read-only RPCかarchive無料。取引・秘密鍵不要。深い履歴はtestnetか小規模プロトコルを使い、RPC枠を超えないようにします

すべて読み取り専用です。 署名も送信も支出もありません。

三つの基準を自動assertionにします。

基準1:一日を再実行してもchecksum不変
基準2:manifestに欠落がなく、隣接高さが連続
基準3:原始ログの直接計算と集計結果が各行一致

小さなプロトコルで範囲を決めます。 中程度のイベント量で連続3~4か月を選び、まず工程を完成させます。

先に開始・終了ブロック高を記録し、検証基準にします。

Rawは保存だけ、解読しません。

block_number、log_index、block_time、tx_hash、address、topics、dataを日別に保存します。dataは原始バイトのままです。

raw_logs/dt=YYYY-MM-DD/part-0000.parquet

一時保存、検証、原子的名前変更、manifest更新の順を後で検証します。

上のDDLでmanifestを作ります。

区画ごとに高さ、行数、checksum、decoder版、状態を記録します。

欠落検索をすぐ実行します。未処理の日が多数返るのが正しい状態です。 欠落を検出することを本番前に確かめます。

DecodedはローカルRawだけを読みます。

ABIでtopicsとdataを解読します。RPCが必要ならRawの保存不足です。

金額は整数か高精度decimalで扱い、浮動小数点を使いません。 小さな誤差でも照合では問題になります。

基準1:一日を再実行します。

異なる三日を再実行し、checksumを比較します。

三回とも元のchecksumと完全一致すること

不一致ならnow()、不安定な順序、updated_atなど実行時情報の混入を調べます。前二つが代表的な原因です。

基準2:欠落を意図的に作ります。

中間日の区画を消し、manifestは残します。集計は静かに一日分を失います。

manifestの行も消して欠落検索します。今度はその日を返さなければなりません。

ファイルは静かに消えますが、manifestで欠落を判定できます。

Curatedで月次を集計し、定義を書きます。

SQL先頭で六つの定義問題に答え、12か月集計を実行します。

基準3:Rawと照合します。

一日を選び、curatedを使わず原始ログから計算します。一致が必須です。

違いはzero addressの除外など定義であることが多く、この検証は定義を明確にする訓練です。

効果を測ります。 同じ検索を業務DBと列指向で実行し、時間、走査量、保存量を比較します。

自分で測った表を作ります。 具体値はデータ次第ですが、桁の差を実際に確認します。

AI Lab

AI LabAIに分析SQLを書かせ、定義を原始イベントと照合するLevel 2 · AI Copilot

表、定義注釈、サンプルをモデルへ渡し、三段階で進めます。

第一段階:
decoded_transfersの構造と、dt分割・block_number順の保存を示します。
過去12か月の月間アクティブアドレスと手数料を計算してください。
pruningを使い、何区画読むか説明してください。

第二段階:
検索の定義上の仮定を少なくとも六つ示してください。
「Xを仮定した。実際がYなら結果はどう変わるか」の形にしてください。

第三段階:
この契約は昨年6月に更新され、イベントのフィールドとtopic0が変わりました。
前後で検索はどう振る舞い、どう修正しますか?

第一段階は何度も書かれたSQL問題なので、通常は得意です。注目するのは分割keyを関数で包んでいないかです。正しい同一結果でも費用が数十倍になる誤りです。 結果だけでは見抜けません。

第二が本題です。モデルは定義を黙って決めがちです。 zero addressや両側集計の仮定を公開させて初めて訂正できます。

第三はアドレスが安定した型識別ではないという問題です。T16 の説明どおり、高さごとに解読し、decoder_versionで管理します。unionを足すだけでは、前後の意味を加算できるか説明したことになりません。

最後は自分でRawから一日分を直接計算します。一致して初めて報告へ使えます。

AI の回答後、自分で必ず検証すること

  • dtを関数で包んでいないか。実行計画で走査区画数を確認する
  • 送信者だけか両側か、zero address除外などを定義注釈と照合する
  • 失敗取引を除外しているか。取引表ならstatusの明示条件が必要
  • 浮動小数点や異なるdecimalsの直接加算がないか
  • date_truncのUTCとsession timezoneを確認する
  • joinの行増殖を一日分の手計算で調べる
  • AIの検索を使わずRawから直接計算し、同じ値を得る

実例

decoder一行の変更で履歴と新規の定義が分裂する

符号付きであるべき金額をunsignedとして読んでいたチームが、当日以降だけ修正しました。

年度照合で過大な月を見つけ、各行のdecoder版が残っていないため調査に二週間かかりました。

manifestの decoder_version は、decoder変更後に一つのSQLで再処理すべき全partitionを列挙するためにあります。再処理後のchecksum変化で影響も確認できます。この列がなければ、全量再処理か記憶に頼るしかありません。

now()分割で再実行後に倍増

初回は今日、再実行は明日の区画に入ります。

両方が残り、各区画は正しくても集計は倍になります。

分割キーは実行環境ではなくデータ自身の属性です。 T16 の原則が倉庫で大きな差になります。

原子的名前変更なしで半分の区画が残る

コンテナが途中停止し、正式パスに半分のファイルが残りました。

検索はエラーなく動き、一日の数が六割減りました。四半期報告で初めて判明します。

半分のファイルは欠落より危険です。 完全なものとして使われるため、一時保存と原子的変更が必要です。

大量ログの一取引で再起動ループ

一取引を一batchにした工程が、大量ログでメモリ不足となり、再起動して同じ取引を読み停止を繰り返しました。

batchは取引やブロックではなくbytesか行数で制限します。 再試行には回数とbackoffも必要で、manifestのattemptsが役立ちます。

変数を一つ変える

もしデータ量が100倍になる

単機では検索メモリと区画ファイルサイズが先に限界になります。

最初の二手は構成変更がほぼ不要です。 chain・contract分割を追加し、よく使うcurated集計を厚くし、最後に分散化します。

機械を増やす前に実行計画を見ます。 大量問題の多くはpruning不成立です。

もし鮮度を時間単位から秒単位へ上げる

safeHeightを小さくすると不変の保存へ再編リスクを持ち込みます。

二つのシステムを併存させます。倉庫は遅れて不変、業務DBと T18 がリアルタイムを担当し、境界を明示して結合します。

リアルタイム部分は修正されます。確定部分と変動部分を利用者へ示すことは T17 のリスク別分類と同じです。

もし複数チェーンに対応する

chain_idを全主キーとパスへ通す作業は機械的です。

難しいのは比較可能な定義です。ブロック頻度、手数料単位、同じ資産のアドレスが違うため、直接合計は誤りがちです。

各チェーンを独立計算し、最後の結合層に定義を書きます。 資産同等性は次章です。

もし外部分析サービスの表が変わる

探索だけなら半日の手間、本番依存なら障害です。

違いは自分で再実行できるRawがあるか。あれば数時間の再処理、なければ相手待ちです。

地味なRawほど省いてはいけません。

持ち帰る問い

1
どんな問題を解決するか?

数字を再計算できる問題を解きます。難しいのは検索ではなく区画、冪等性、由来で、数字を疑われる日に価値が現れます。

5
誰が支払っているか?

archive RPC、保存容量、走査bytesの三費用があります。通常最も高い取得費を一回にするのが層分離です。全量再取得は最も高い項目の再払いです。

17
AI が間違えたとき、損失を負うのは誰か?

AIが誤ると誰が損失を負うか?このLabで特に起きるのは、ゼロアドレスを含むか、送信側と受取側の双方を含むかを、モデルが黙って定義を決め、権威あるように見える数値を返すことです。エラーもrevertもなく、そのまま配布される報告になります。署名して報告を出す人が責任を負います。

章末チェック

要点

オンチェーンデータ工学の要は、巧妙なクエリではなく、冪等で再実行可能な ETL だ。

この章の実践課題を終えたらチェックしてください進捗をすべて見る

このページの内容