Hyperliquid API の接続と認証── API ウォレットと nonce で発注の権限を設計する
API キーの代わりに、出金できない署名専用の鍵を発注プロセスごとに割り当てます。鍵の分け方と nonce の扱いを最初に決めておけば、本番で原因の分からない拒否に悩まされません。

分散型取引所(DEX)へのトランザクションの送信は、平時には次のブロックか数ブロックのうちに取り込まれ、問題なく動いているように見えます。崩れるのは相場が急に動いた日です。手数料が足りずに送信が滞留し、後続の送信もまとめて止まり、相場の購読が切れて状態が古いまま判断を続けます。どれも障害ではなく、ブロックチェーンの仕様どおりの動作です。
本稿では、Ethereum とその互換チェーンを前提に、手数料の上がり方、滞留した送信の置き換え、nonce の抜けによる詰まり、購読が切れたときの戻し方、再編成と確定の扱いを、仕様と代表的なノード実装(geth)の既定値をもとに整理します。統合方法の選び方と送信前に決めることは前稿で扱っています。
EIP-1559 以降、手数料はブロックごとに決まる基準額(base fee)と、送信者が上乗せする優先手数料の 2 段構えです。基準額は、直前のブロックが目標の使用量(ブロック上限の半分)を超えると上がり、下回ると下がります。上がり幅は超過分に比例し、ブロックが上限まで埋まったときに最大で 1/8、つまり 12.5% 上がります。
送信者は、トランザクションに 1 ガスあたりの支払い上限(max_fee_per_gas)と優先手数料の上限(max_priority_fee_per_gas)を書きます。そのブロックの基準額が支払い上限を超えていると、そのブロックには取り込まれません。実際に払うのは基準額と、上限の範囲内の優先手数料の合計で、上限との差額は払いません。基準額の分は誰にも支払われず焼却されます。
設計で効くのは、混雑が続くと基準額が複利で上がる点です。満杯のブロックが 6 回続けば、基準額は 1.125 の 6 乗で約 2 倍になります。直近の基準額ぎりぎりに支払い上限を置くと、混雑の始まりで取り込まれなくなります。
手数料が足りずに滞留した送信は、同じ送信元から同じ nonce で、手数料を上げたトランザクションを送ると置き換えられます。どちらか一方しか取り込まれないので、二重に約定する余地がないのが置き換えの利点です。新しい nonce で送り直すのは別の操作で、元の送信が後から取り込まれれば両方が成立します。
置き換えを受け付ける条件は、ノードの取引プールの設定で決まります。geth の既定値では、既存のトランザクションを置き換えるには価格を 10% 以上上げる必要があります(--txpool.pricebump)。少しずつ上げて何度も送る実装は、この条件に届かずに拒否されます。
取り消したい場合も同じ仕組みを使います。同じ nonce で、価値を動かさないトランザクション(自分宛ての 0 の送金など)を高い手数料で送れば、元の送信は取り込まれなくなります。ただし、元の送信が先に取り込まれれば置き換えは失敗します。置き換えは「必ず効く取消」ではないので、どちらが取り込まれたかを確定の段階で確かめます。置き換えるたびにトランザクションのハッシュは変わるので、業務側の識別子には nonce と送信元を紐づけ、同じ nonce のハッシュをすべて記録しておきます。

同じアカウントからの送信は、nonce の順にしか取り込まれません。途中の番号が抜けていると、それより後の送信は実行できない列に入って待ちます。geth の既定値は次のとおりです。
--txpool.accountslots)、全体で 5,120 件--txpool.accountqueue)、全体で 1,024 件--txpool.lifetime)つまり、抜けを放置すると後続は実行されないまま最長 3 時間で捨てられます。手元では送信済みのつもりでも、チェーン上には何も起きていない状態です。ノードの再起動や、別のノードへの送信で、プールの中身が失われることもあります。
対策は、nonce を自分で採番して記録し、チェーン上で確定した nonce と突き合わせることです。確定した番号と送信済みの番号の差が一定時間縮まらなければ、抜けか滞留です。最も小さい未確定の番号から置き換えで埋めます。前稿で触れたとおり、送信するアカウントを用途ごとに分けておけば、詰まりの影響はそのアカウントの中に閉じます。
新しいブロックやイベントのログを WebSocket で購読(eth_subscribe)している場合、geth の説明では、購読は接続に結び付いており、接続が切れるとその上の購読はすべて消えます。通知されるのは現在のイベントだけで、過去のイベントは届きません。さらに、受け取り側の処理が遅れて未送信の通知が 1 万件に達すると、接続そのものが切られます。
したがって、再接続して購読し直すだけでは、切れていた間のイベントが抜けます。再接続の手順は次のようにします。
eth_getLogs で取得して埋める購読の取り込みが遅れて接続を切られる、という経路もあるので、受け取った通知はまず列に積み、重い処理は別に回します。受け取りの処理の中で判断や送信まで行う作りは、混雑の日ほど遅れて切断を招きます。
ブロックは、確定する前に別のブロックに置き換わること(再編成)があります。geth の購読では、再編成が起きると新しいチェーンの先頭のブロックが通知され、古いチェーンにあったログは removed が true の状態で送り直されます。eth_getLogs の結果でも、removed は再編成で取り除かれたログを示します。
取り込み処理は、removed が立ったログを受け取ったら、そのログで反映した内容を元に戻す必要があります。追加だけを前提にした実装は、再編成のたびに残高や約定の記録がずれます。
どの時点の状態を信じるかは、JSON-RPC のブロック指定で選べます。latest は直近に提案されたブロック、safe は安全とみなされた先頭のブロック、finalized は確定したブロックです。Ethereum の proof of stake では、12 秒のスロットを 32 個まとめたエポックの区切りで、検証者の 3 分の 2 以上の投票によってチェックポイントが確定します。金額の小さい画面表示は latest、残高の照合や出金の判断は finalized というように、用途ごとに参照する時点を決めておきます。
1 エポックは 12 秒 × 32 で 6.4 分です。finalized を待ってから次に進む処理は、送信から分単位で遅れる前提で組みます。秒単位で次の判断が要る処理は、latest で進めつつ、確定の段階でずれを検知して直す、という 2 段構えにします。
DEX への送信は、混雑した日に詰まる前提で設計します。基準額は満杯のブロックが続くと 1 ブロックで最大 12.5% ずつ上がるので、支払い上限は待てるブロック数ぶんの上昇を見込みます。滞留した送信は同じ nonce で手数料を 10% 以上上げて置き換え、新しい nonce での送り直しはしません。nonce の抜けは後続を止め、geth の既定では最長 3 時間で捨てられるので、確定した番号と突き合わせて埋めます。購読は切れると過去分が戻らないので、最後に処理したブロックから取得して埋め、再編成で removed が立ったログは逆に戻します。参照する時点は用途ごとに latest・safe・finalized から選びます。
金融テクノロジー総合研究所では、DEX を含むオンチェーン連携の設計・実装を受託しています。送信と取り込みの処理の点検や、混雑時の挙動を前提にした再送の設計からご相談いただけます。Crypto DEX API 開発の詳細もあわせてご覧ください。ご相談はお問い合わせよりご連絡ください。
latest・safe・finalized、ログの removed)FTL の技術スタックと受託開発の進め方は FTLの技術とは? にまとめています。