T15 · Crypto のバックエンド
チェーンにデータがあるのに、なぜバックエンドが必要か?
- 練習する能力
- 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 で表構造に落とし込みます。
モデルを作る
チェーンの両側に、異なる失敗を持つ経路があります。
- 読み取り:RPC と索引 DB
- Cache:キャッシュ
- API:集約
- フロントエンド
- 書き込み:要求
- Idempotency:重複確認
- Queue:投入
- Worker:送信
- Confirm:レシート確認
- 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 です。RPC の透過転送ではなく、ページに必要な形で、一要求に全必要データを返します。
一画面の描画に何要求必要かで判断します。3 回を超えるなら、この層が不十分です。
計算済みの答えを保存します。Crypto では、有効期間を時間ではなくブロック高が定めることが特徴です。
経験則は、確定済みを保存し、変わるものを不用意に保存しないことです。過去ブロック、確定レシート、メタ情報は保存できます。最新残高は高さ付きか短い TTL にします。
要求の受付と実行を分け、再試行、負荷平準化、優先度、失敗の隔離を可能にします。
少なくとも一度の配送と、順序が変わる可能性を理解します。消費側は冪等で、順序に依存しない必要があります。
クライアントが生成する、同じ業務意図を表す文字列です。繰り返し送っても一度だけ実行し、同じ結果を返します。
要件は、クライアントによる生成、業務上の一意性、明確な有効期限です。サーバー生成では、再試行で新しいキーになります。
同時に同じキーを取得するのは一要求だけにし、残りが待って結果を共有します。
失効直後の集中取得を防ぎます。これがないキャッシュは、最も必要なピーク時に最も機能しません。
何度再試行しても失敗するメッセージを置き、人の対応を待ちます。無限再試行や黙った破棄を避けます。
資金に関わるキューには必須です。 送金メッセージの黙った破棄は、誰も気付かないためエラーより深刻です。
実践
すべてテストネットで行い、書き込みもテストネット取引にします。 秘密鍵が必要なら、テストコインのみを持つ一時ウォレットを使います。
目標は経験値を信じることではなく、自分で測った比較表を作ることです。
素の 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
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 の一意インデックスが信頼できます。キャッシュは、不変データをローカル、変わるデータを共有と層分けできます。
割当と制限の制約が消え、遅延も減ります。キャッシュの効果は減っても、集約計算の費用は残るので、なくなりません。
次はノード同期を監視します。数十ブロック遅れたノードは、エラーなしで古いデータを返します。 高さを確認し、遅れが閾値を超えたら切り替えか通知を行います。
T4 の説明通り、自前は万能ではなく、割当問題を運用問題へ変えます。
持ち帰る問い
誰が支払いますか?照会費用はユーザーではなく運営者です。重複する上流呼び出しを一つ減らすたび、限界費用が直接下がります。 バックエンドの大きな存在理由です。
誰がリスクを負いますか?冪等性が弱ければ、二重支払いするユーザーです。低頻度、偶発、再現困難で、苦情まで表れないことが多い問題です。レビューではなく同時実行テストが必要な理由です。
何を解決しますか?信頼はチェーンの役割です。バックエンドは体験、費用、信頼性を解決します。混同すると、バックエンドが真実を決める設計になります。
章末チェック
集約、キャッシュ、再試行、通知、レート制限、キー保管、冪等性という 7 つです。
事実の内容ではなく、事実を効率良く確実に届ける方法に関わります。
事実源はチェーン、バックエンドは投影です。全数値を特定の高さへたどれるようにします。
同時要求が両方「なし」を読んで挿入するからです。確認と書き込みの間に隙間がある、典型的な競合です。
一意インデックスと挿入競合処理で、二工程を一つのアトミック操作にします。成功なら初回、競合なら重複です。
同時実行でも成立するかを問い、推測ではなくテストで答えます。
業務ロジックの一回と、nonce によるチェーンの一意性が違うためです。
送信後、レシート前に worker が異常終了し、再起動後に新しい nonceで再構築すると、両取引が合法で入り、両方支払います。
キーを一つの nonce に固定し、再構築でも同じ nonce にすれば、最大一つだけ残ります。
データの性質で決めます。
- 不変な桁数、過去ブロック、確定レシートは、高さなしで長期保存します。
- 高さごとに確定する残高、状態は、高さが必須です。その高さの古い誤答を返しません。
- 常に変わるメモリプールや見積もりは、短い TTL か保存なしです。
高さ更新でヒット率が下がるので、過去照会向きです。最新なら、短い TTL とシングルフライトが通常有利です。
唯一の正解はありません。次を確認します。
- 各キャッシュキーに高さがあるか?なければ、いつ古いデータを返すか?
- 要求から送信まで、どこで重複が起き得るか?すべて防いだか?
- キューの配送保証は何か?消費側は冪等か?デッドレターキューはあるか?
- 上流制限はどこか?割当切れでは旧データを返すか、エラーか?
- バックエンドが計算し、チェーンから再導出できないデータはあるか?あれば将来の不一致の場所です。
第 5 項が T16 への導入です。
要点
バックエンドはチェーンが苦手な集計、キャッシュ、再試行、通知、レート制限を担う。