Crypto OS
Technical Crypto OS第3段階 · フルスタックのオンチェーンアプリケーション

T15 · Crypto のバックエンド

チェーンにデータがあるのに、なぜバックエンドが必要か?

この章を 3D で体験する →
練習する能力
Builder
実践
オンチェーン照会にキャッシュとキューを追加し、有無による遅延の差を負荷テストで比べる。
AI Lab
AI に冪等キーの仕組みを設計させ、重複リクエストを作って本当に冪等か検証する。

現実の問題

「自分の資産」ページの要件は簡単です。保有トークンを一覧にし、残高、現在価格、24 時間の変動を表示します。

20 種類持っていると仮定して数えます。

  • 残高照会 20 回
  • 小数桁数照会 20 回(トークンによって異なる)
  • 名前とシンボル照会 20 回
  • 別のデータ源から 20 の価格

ブラウザーから 80 回以上のネットワーク要求が送られます。

初期表示に 6 秒かかり、壊れたと思って更新するとまた 80 回です。3 人が同時に更新すると RPC の割当が 429 を返します。サイトには 3 人しかいないのにです。

さらに考えると、結果はすべてのユーザーで同じです。名前、シンボル、小数桁数は変わらず、価格も共通です。同アドレスの同ブロック高の残高も確定しています。

全員に、世界中で同じ答えをそれぞれ計算し直させています。

バックエンドが必要な理由は、もはや哲学ではありません。チェーンが行わず、フロントエンドではうまく行えず、バックエンドが担うしかない仕事があるからです。

思考実験

RPC を、窓口が一つしかない資料館として考えます。

特徴は次の通りです。

  • 読み取り専用で、厳密です。 聞いたことだけ答え、集計や並べ替えをせず、「このアドレスの一か月の純流入」も教えません。
  • あなたを覚えません。 毎回最初からで、セッションも文脈もありません。
  • 順番待ちがあります。 全体で列を共有し、一人が頻繁に聞くと全員が待ちます。
  • 回数ごとの料金です。 聞くほど高くなります。

製品の要件を窓口へ持っていきます。

「20 トークンの残高をドル価値順に」なら、自分で取得して並べます。 「入金したら通知して」なら、繰り返し聞きます。 「3 回押した引き出しは一度だけ」なら、窓口は業務上の一度を知らず、3 取引を実行します。 「一人につき一分で 10 回まで」なら、窓口は人を識別しません。

これが、集約、ポーリングと通知、冪等性、レート制限という 4 つの役割です。同じ問いの答えがしばらく変わらないことから、キャッシュも加わります。

バックエンドはチェーンを置き換えるのではなく、チェーンができない部分を担います。 逆側の境界も重要です。チェーンができること、特に何が真実かの判断を奪ってはいけません。

あなたが決める

資産ページに層を追加します。何から始めますか?

結果を観察する

一つを選ぶのではなく、順に積み重ねる層です。同じ問題の両面を解決します。

補う能力チェーンが行わない理由フロントエンドが苦手な理由
集約単位の小さい問いだけに答える数十要求と計算が必要
キャッシュ前回の質問という概念がないブラウザーごとにしか共有できない
再試行利用側の失敗を知らないページ終了で止まる
通知自発的に連絡しないオフラインでは受け取れない
レート制限と割当ユーザーを識別しない制限を回避できる
キーの保管チェーンとは無関係フロントエンドの秘密は秘密にならない
冪等性業務 ID ではなく nonce を識別する重複クリックを制御しきれない

ここで境界を設けます。

チェーンが事実の源で、バックエンドは投影と流量制御の層です。

キャッシュ、集約、加工、事前計算はできますが、すべての数値をチェーンの特定の高さへたどれる必要があります。独自の残高を増減してチェーンにない事実を作ると、二つの真実ができ、いずれ分かれます。T16 で表構造に落とし込みます。

モデルを作る

チェーンの両側に、異なる失敗を持つ経路があります。

  1. 読み取り:RPC と索引 DB
  2. Cache:キャッシュ
  3. API:集約
  4. フロントエンド
読み取りでは遅延、割当、費用を節約します。読み違いはやり直せます。
  1. 書き込み:要求
  2. Idempotency:重複確認
  3. Queue:投入
  4. Worker:送信
  5. Confirm:レシート確認
  6. Notify:通知
書き込みでは、一要求につきチェーンへの送信を最大一回にします。誤った書き込みはやり直せません。

読み取り:キャッシュの 3 層

キャッシュ自体よりキーの設計が重要です。

種類例キー方針
不変トークン名、シンボル、桁数、過去ブロック、最終確定レシート高さなし長期保存、ほぼ失効なし
高さごとに確定特定アドレスの残高、コントラクト状態高さが必須高さが変わればキーも変わる
常に変わるメモリプール、最新見積もり短い時間窓秒単位 TTL、または保存しない

第一は最大の無料の恩恵です。80 要求中、名前、シンボル、桁数の照会が 40 回あります。一生変わらない値なので、一度保存すれば十分で、要求が半減します。

第二の技法は、キーにブロック高を含めることです。高さが変わればキーが変わり、自然に問い合わせ直すので、古いデータをその高さの答えとして返しません。ただし高さ更新でヒット率が下がり、最新照会より過去照会向きです。

初版で見落としがちなのが、シングルフライトです。100 要求が同時にミスすると、100 回取得し、キャッシュは役立たず、集中負荷を起こします。同じキーは一要求だけが取得し、残りが結果を共有します。

書き込み:冪等性の 3 段階

重複クリック、ネット再送、キュー再配送、クライアント再試行など、重複要求は例外でなく日常です。チェーンでの重複は実際の損失になります。

3 段階すべてで成立させます。

第 1 層  要求の冪等性:同じキーでは一つのタスクのみ
         一意インデックスと挿入競合時の元結果返却で実装

第 2 層  タスクの冪等性:複数回処理しても副作用は一度のみ
         タスク状態機械と条件付き更新を使い、読み取り後の書き込みを避ける

第 3 層  チェーンの冪等性:一業務操作で収録される取引は最大一つ
         キーを nonce に固定して結び付ける

第三は最も漏れやすく、高くつきます。 バックエンドが一度送っても、再起動した worker が新 nonce で再構築すれば、二つともチェーンへ入り成功します。

逆に、キーを nonce に固定すれば、再構築した取引は同 nonce で置き換わり、チェーンへ入るのは最大一つです。チェーンの規則から得る保険です。

要求の冪等性の表構造です。SQL は安定しているので、具体的に示します。

create table submissions (
  idempotency_key text        primary key,
  request_digest  text        not null,       -- 同じキーで異なる内容を識別する要求本文のハッシュ
  status          text        not null,       -- queued | broadcast | confirmed | failed
  nonce           bigint,                     -- 割当後は固定
  tx_hash         text,
  result          jsonb,
  created_at      timestamptz not null default now(),
  updated_at      timestamptz not null default now()
);

先に照会せず、一文で挿入します。

insert into submissions (idempotency_key, request_digest, status)
values ($1, $2, 'queued')
on conflict (idempotency_key) do nothing
returning idempotency_key;
  • 一行返る:新要求なので投入します。
  • ゼロ行:重複要求なので元記録を読み、クライアントへ返します。
  • request_digest が違う:同じキーを別要求に使っています。競合エラーを返します。多くはクライアントのキー生成バグで、黙って受け入れません。

先に SELECT し、なければ INSERT する方法は誤りです。 同時要求が両方「なし」を読み、同時挿入します。一意インデックスが信頼できるロックです。

レート制限は 2 系統

外向けと内向けは別で、半分しか行わない例が多くあります。

  • 下流のユーザー向け:ユーザー、アドレス、IP 単位で悪用を防ぎ、超過時は 429 と再試行時刻を返します。
  • 上流の RPC 割当向け:出口にも制限し、急増で一日の割当が消費され、全体停止しないようにします。

上流では捨てず、待ち行列とタイムアウトを使います。トークンを待ち、得られなければ速く終了して旧キャッシュ値を返します。少し古い答えは、エラーページより有用です。

何と呼ぶか

API LayerAPI 層

自分のフロントエンド向けの API です。RPC の透過転送ではなく、ページに必要な形で、一要求に全必要データを返します。

一画面の描画に何要求必要かで判断します。3 回を超えるなら、この層が不十分です。

Cacheキャッシュ

計算済みの答えを保存します。Crypto では、有効期間を時間ではなくブロック高が定めることが特徴です。

経験則は、確定済みを保存し、変わるものを不用意に保存しないことです。過去ブロック、確定レシート、メタ情報は保存できます。最新残高は高さ付きか短い TTL にします。

Queueキュー

要求の受付と実行を分け、再試行、負荷平準化、優先度、失敗の隔離を可能にします。

少なくとも一度の配送と、順序が変わる可能性を理解します。消費側は冪等で、順序に依存しない必要があります。

Idempotency Key冪等キー

クライアントが生成する、同じ業務意図を表す文字列です。繰り返し送っても一度だけ実行し、同じ結果を返します。

要件は、クライアントによる生成、業務上の一意性、明確な有効期限です。サーバー生成では、再試行で新しいキーになります。

Single Flightシングルフライト

同時に同じキーを取得するのは一要求だけにし、残りが待って結果を共有します。

失効直後の集中取得を防ぎます。これがないキャッシュは、最も必要なピーク時に最も機能しません。

Dead Letter Queueデッドレターキュー

何度再試行しても失敗するメッセージを置き、人の対応を待ちます。無限再試行や黙った破棄を避けます。

資金に関わるキューには必須です。 送金メッセージの黙った破棄は、誰も気付かないためエラーより深刻です。

実践

実践チェーン照会へキャッシュとキューを加え、負荷試験で遅延を比較する任意のバックエンドフレームワーク + データベース + 負荷試験ツール0 円、すべてテストネットで実施

すべてテストネットで行い、書き込みもテストネット取引にします。 秘密鍵が必要なら、テストコインのみを持つ一時ウォレットを使います。

目標は経験値を信じることではなく、自分で測った比較表を作ることです。

素の API を書き、基準を測ります。

GET /portfolio?address=... 内で数十回 RPC を呼び、集約して返します。

50 同時要求を 30 秒行い、3 数値を記録します。p50、p95、上流 RPC の総呼び出し数です。

3 番目は請求額へ直結するので最重要です。

不要な半分の照会を削ります。

トークン名、シンボル、桁数を永続キャッシュにします。不変なので初回の値を使い続けられます。

再測定すると、呼び出しが約半減するはずです。業務ロジックは一行も変えていません。

変わるデータは高さ付きキーにします。

chainId:method:params:blockNumber を使い、各要求で最新高を一度取得して照会します。

再測定して p95 を比較し、高さ更新ごとにヒット率がどう変わるかも観察します。

シングルフライトを加え、有効か確認します。

同じキーの同時取得を一要求だけにします。

キャッシュを空にして、同じ要求を瞬時に 100 回送り、上流の実呼び出し数を数えます。1 が正解です。100 なら機能していません。

書き込みを非同期にします。

POST /withdraw で検証し、submissions へ記録し、投入後直ちに 202 とタスク ID を返します。

GET /withdraw/:id で状態を照会し、フロントエンドでポーリングします。T14 の状態機械へ接続できます。

冪等キーを追加し、重複要求で攻撃します。

ヘッダーにキーを求め、前の on conflict do nothing を使います。

攻撃 1: 同キーを順番に 10 回送り、タスクは一つ、後の 9 回は同 ID を返すことを確認します。 攻撃 2: 同キーを同時に 10 回送り、同じ結果を確認します。先に照会する実装を破るテストです。 攻撃 3: 同キーで異なる本文を送り、初回内容を黙って実行せず、競合エラーを返すことを確認します。

通過後、チェーンを数え、取引は一つだけと確認します。

worker を一度終了させます。

送信済みでレシート未取得のときに、プロセスを kill し、再起動します。

submissions から送信済みとハッシュ X を復元し、新取引を構築せずレシートを待つべきです。

二つ目があれば、第 3 層の nonce 固定ができていません。本 Lab で最も価値の高い工程です。

比較表を作ります。

キャッシュなし、あり、キャッシュとキューありの 3 行、p50、p95、上流呼び出し数の 3 列です。

段階の成果物の一部であり、自分の測定なので、将来アーキテクチャを議論する際の強い材料にもなります。

AI Lab

AI LabAI に冪等キー方式を設計させ、重複要求で実際の冪等性を確認するLevel 2 · AI Copilot

2 段階で進めます。2 番目はチェーンを問うため、モデルが失敗しやすい箇所です。

第 1 段階:
チェーンへ取引を送るバックエンド API の冪等方式を設計してください。
- キーの生成者、形式、有効期限
- 保存構造と同時実行の正確性保証
- 重複要求で返すもの
- 同じキーで異なる本文の場合
- 失敗後の再試行と終状態の判定
具体的な表構造と SQL を示してください。

第 2 段階:
worker が送信済み、レシート未取得で異常終了し、再起動したとします。
この記録を読んだ後、何をすべきですか?
取引を再構築して送った場合、チェーンでは何が起きますか?
一業務要求でチェーンへ入る取引を最大一つにするには、どうしますか?

第 1 段階はもっともらしく答えますが、先に SELECT して存在判定していないか注視します。単一処理では動き、負荷下で破綻する、よくある見せかけの冪等性です。

第 2 段階が分かれ目です。多くの教材は Web バックエンドで止まり、チェーンの nonce を扱いません。答えられなければ、モデルの知識の境界を見たことになります。冪等性は知っていても、この分野の追加の一層を知らないのです。

実装後、自分で同時実行テストを行います。 コードを読むだけでは検証できず、負荷を掛ける必要があります。verify の最初の 3 項目が実行を求める理由です。

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

  • 照会してから挿入ではなく、一意インデックスと挿入競合処理を使っている。前者は同時実行で破綻します
  • 同時に二要求が届いても一つだけ成功するか、同時実行テストを自分で書いて実行した
  • 重複要求が、業務を再実行せず初回結果を返す
  • 同じキーで異なる本文を扱う。多くの案は漏らします
  • キーと取引 nonce を固定して結び付けている。バックエンドの一度は、チェーンの一つとは限りません
  • 失敗後の再試行で、再試行可能と終状態を区別し、キー再利用の可否を定めている
  • DB 構文、ロック、キューの配送保証を、自分の使用版の文書と一つずつ照合した

実例

キューの再配送で二重配布した

エアドロップを処理し、取引が入った後、受領確認を送る前に worker が再起動しました。

キューは未処理と見なして再配送し、別 worker が再送しました。一人に二回配布しました。

少なくとも一度を、正確に一度と誤解したのが原因です。キュー製品の変更ではなく、消費側の冪等性で防ぎます。

未確定の状態をキャッシュした

取引状態を TTL 10 分で保存し、pending の照会結果が入りました。

20 秒で確定しても、10 分間 pending が返り、処理中を見たユーザーが止まったと考えて再送しました。

教訓:TTL をデータの変化速度へ合わせます。 中間状態は保存しないか、予想変化時間より短い TTL にします。

一つのクローラーが一日の割当を消費した

上流制限のないサービスへ、収集スクリプトが半時間要求し、一日の RPC 割当を使い切りました。

予告なく全ユーザーの要求が失敗しました。割当は坂ではなく崖です。

教訓:上流制限は節約だけでなく、最悪時の可用性のためです。 安全余裕を残し、閾値近くで通知します。

バックエンドが第二の事実源になった

性能のため独自残高を維持し、操作ごとに増減して、チェーンと照合しなくなりました。

2 か月後、数百アドレスが一致しませんでした。失敗取引の見落とし、内部送金の漏れ、再編成の巻き戻し漏れなどが原因でした。

教訓:事実のキャッシュはできても、チェーンの事実を作ってはいけません。 派生データは再計算可能にし、定期照合します。T16 で表設計の必須ルールにします。

変数を一つ変える

もしキャッシュだけ追加し、キューは追加しない

読み取りの性能と費用は改善します。表示中心の多くの製品では十分です。

しかし書き込みは同期要求のままです。ユーザーは回転を見て、バックエンドはレシートを待ち、失敗すればエラーしか返せません。再クリックによる再試行は、重複送信も招きます。

読み取り専用ならキューは不要でも、代理送信するならキューと冪等性を同時に導入します。

もしキューだけ追加し、キャッシュは追加しない

書き込みは安定しても、読み取り費用は減りません。

さらに worker が状態、nonce、レシートを大量照会し、キュー導入が上流負荷を増やします。

両者は補完的です。キャッシュは費用、キューは信頼性を改善します。請求額と事故のどちらが問題かで、順序を決めます。

もしサービスが単一から複数インスタンスになる

プロセスメモリのものはその場で成立しなくなります。N 個の異なるキャッシュができ、メモリの冪等表は共有されず、シングルフライトは一台内だけです。

分散への典型的な崖で、エラーを出さず、まれな重複が始まります。最も調査しにくいバグです。

冪等状態は共有ストレージへ置きます。DB の一意インデックスが信頼できます。キャッシュは、不変データをローカル、変わるデータを共有と層分けできます。

もし上流を公開 RPC から自前ノードへ変える

割当と制限の制約が消え、遅延も減ります。キャッシュの効果は減っても、集約計算の費用は残るので、なくなりません。

次はノード同期を監視します。数十ブロック遅れたノードは、エラーなしで古いデータを返します。 高さを確認し、遅れが閾値を超えたら切り替えか通知を行います。

T4 の説明通り、自前は万能ではなく、割当問題を運用問題へ変えます。

持ち帰る問い

5
誰が支払っているか?

誰が支払いますか?照会費用はユーザーではなく運営者です。重複する上流呼び出しを一つ減らすたび、限界費用が直接下がります。 バックエンドの大きな存在理由です。

9
誰がリスクを負うか?

誰がリスクを負いますか?冪等性が弱ければ、二重支払いするユーザーです。低頻度、偶発、再現困難で、苦情まで表れないことが多い問題です。レビューではなく同時実行テストが必要な理由です。

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

何を解決しますか?信頼はチェーンの役割です。バックエンドは体験、費用、信頼性を解決します。混同すると、バックエンドが真実を決める設計になります。

章末チェック

要点

バックエンドはチェーンが苦手な集計、キャッシュ、再試行、通知、レート制限を担う。

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

このページの内容