マネーパスの強化:KVR 決済のトランザクションセキュリティ

支払い署名リプレイ、無認証の報酬鋳造、レースによる二重支払い——三つの実在する脆弱性クラスをゲートウェイの決済サービスで発見し、テストで実証し、封じた。

DePIN では、マネーパスは計算パスと同じだけ敵対的だ:KVR を計上するすべてのエンドポイントは、いずれ働かずに KVR を得たい誰かに突かれる。オンチェーン支払いを検証し、ステーキング・ノード報酬・推論料金を計上するゲートウェイ決済サービスへのセキュリティ点検で、三つの実在する脆弱性クラスを発見し封じた。それぞれ修正前にエクスプロイト式テストで実証し、修正後に再検証した。

信頼モデル

クライアントの主張ではなくオンチェーンの事実を検証する

Kvasir のカストディモデルは鍵をユーザーの手元に置く:ウォレットがトランザクションに署名し、Solana がそれを記録し、決済サービスの仕事は残高に触れる前にチェーン上で実際に何が起きたかを検証することだ。支払いは *quote → payment → inference* に従い、消費された全トランザクション署名はワンショットの usedSignatures レジストリに記録され、二度と提示できない。これにより決済サービスが関門となる——決して破ってはならない規則はひとつ:チェーンが証明したものだけを計上し、クライアントが主張したものは決して計上しない。ただし、このサービスは devnet では資金を保有する:オンチェーンのステーキングプログラムが提供されるまで、ステークされた KVR とプリペイドクレジットはそのトレジャリーに置かれ、台帳で管理される。

A settlement vault guarded by three locks: sender binding, trusted reporter, and a serialization gate
修正 #1 · 送信者バインディング

支払いを支払人に結びつける

Solana の署名は公開だ。ステーキング検証パスは vault が期待どおりの KVR を*受け取ったか*だけを確認し——*誰が送ったか*は確認していなかった。攻撃者は devnet で被害者の KVR→vault 送金を見張り、{owner: 攻撃者, signature: 被害者のもの} を提出すればよかった:vault 受領チェックは通り、元本は攻撃者に計上され、unstake 一回で資金は攻撃者のものになる。ブロックエクスプローラだけで可能な直接的窃盗だ。

修正:KVR は計上対象の owner が所有するトークンアカウントから引き落とされていなければならない。

verifyStakeTransfer(signature, owner, amount):
  delta(vault)  >= amount            # vault actually received it (old check)
  Σ debits from token accounts
    whose owner == credited owner    # NEW — sender binding
                >= amount            # summed across that owner's accounts
  # inference path (no owner): bound by private requestId
  # + one-shot usedSignatures instead
修正 #2 · 信頼されたレポーター

報酬は認証された情報源からのみ

ノード報酬のエンドポイントは自己申告の入力から請求可能な KVR を鋳造していた:POST /api/node/contribution はクライアントが主張する units をそのまま計上し——units: 1e9 と claim 呼び出し一回で vault を空にできた——register/heartbeat は自称の hub/ゲートウェイ役割(毎時のインフラ報酬)と性能スコア(報酬乗数)を鵜呑みにしていた。修正は、報酬に影響するすべての主張を信頼されたレポーターの背後にゲートする:hub の貢献ポーリングが使う M2M サービストークン、または認証済み管理者だけが units・インフラ役割・性能ティアを主張できる——これらは KVR を鋳造するため、オープン LAN モードでも強制される。トークン比較は定数時間で、ウォレット↔ノードの紐付けは今も自由——自分の報酬を自己主張できなくなっただけだ。

修正 #3 · 決済の直列化

すべての残高に書き手はひとり

決済状態はロックなしの read-modify-write で、すべての資金操作は途中でオンチェーンの支払いか検証を *await* する——古い残高を握ったままイベントループを譲るのだ。並行する二つの claim が同じ 100 KVR の残高を読み、両方が支払えてしまう。理論ではなかった:エクスプロイトテストで並行 claim 3 件が 100 の残高に対し 300 を支払った。

キーごとの非同期直列化:同一キーの資金操作は厳密に順番に実行される。

withLock(key, fn)         # per-key promise chain, self-cleaning map
  stake / unstake / claim  → keyed by owner
  inference settlement     → keyed by requestId
inside the lock:
  usedSignatures check + credit   # no same-signature double-credit
  pay out FIRST, then debit       # failed payout leaves balance intact
多層防御

各層の現在地

層メカニズム
アイデンティティサーバー nonce への SIWS ウォレット署名ログイン + TOTP 2FA + 使い捨てバックアップコード
トランスポートシャードダウンロードのウォレット由来 node token;リレーのビルドフィンガープリント一致
支払いステーキング送金の送信者バインディング;ワンショット usedSignatures;推論の非公開 requestId
決済すべての残高書き込みを包むキーごとのロック;先払い後引き落とし;冪等な再提出
レポーティング報酬に関わる事実は M2M サービストークンか管理者のみ、定数時間比較
カストディノンカストディアルウォレット——サービスが動かせるのは vault の中身だけで、ユーザーの鍵には決して触れない
仮定ではなく測定

すべての修正に専用のエクスプロイトテストが付く

  • 被害者の送金署名を攻撃者のウォレットでリプレイする試みは今や拒否される("not sent by owner");正当なステーキング・過剰請求・推論支払いは変わらず動作する。
  • 一つの残高への並行 claim 3 件はちょうど一度支払われ、支払い済み推論の再提出は冪等に同じ結果を返す。
  • 無認証クライアントの自称 units・hub/ゲートウェイ役割・性能ティアは、もはや報酬を 1 lamport も動かせない。

三つの修正を貫くのは、ひとつの原則の三通りの適用だ:チェーンが真実の源であり、サービスは検証者であり、すべての残高に書き手はちょうどひとり。決済サービスはまだ Solana devnet で動いている——メインネットが賭け金を引き上げる前に、この種のクラスを発見し、突いて、直すのにまさにふさわしい場所だ。