Skip to content

Settlement の二重送金を防ぐ原子的claim(review_passed→settling etagロック) - #4

Merged
Mameta29 merged 1 commit into
mainfrom
dogfood/settlement-race-fix
Jun 14, 2026
Merged

Mameta29 merged 1 commit into
mainfrom
dogfood/settlement-race-fix

Conversation

@Mameta29

Copy link
Copy Markdown
Owner

Closes #3

概要

Settlement の二重送金を構造的に不可能にする修正です。dogfood #1(PR #2)の
運用中に、実際に 1000 JPYC が2回送金される二重送金を観測しました(Amoy block
40122642 / 40122643)。本 PR はその根本原因を断ちます。

根本原因(dogfood #1 で観測)

Settlement の冪等性ガードが read-then-act(TOCTOU)でした。2つの Settlement
(ローカル駆動と closed(merged) webhook)が ~1.5s 並走し、両方が「まだ未送金」を
同時に読んでから、両方とも transfer() してしまいました。txHash 存在チェックは
送金完了後にしか書かれないため、並走窓では効きません。

修正

送金の直前に review_passed → settlingetag 楽観ロック(既存 transitionOrder
の IfMatch)で原子的に確保します。並走する2つの Settlement のうち claim に勝てるのは
1つだけで、敗者は canTransition / etag 412 で送金前に弾かれます。転送失敗時は
settling → review_passed にロールバックしてリトライ可能にします。

検収基準への対応

  • 既存テストがすべて通る(全31テスト通過)
  • CIが通過している(build / typecheck / vitest)
  • 二重送金を防ぐ並走テストがあるsettlement.test.ts: 2つの runSettlement
    が同一 order を並走しても transfer はちょうど1回、を検証
  • TypeScriptの型エラーがない(tsc --noEmit 全パッケージ通過)
  • テストが追加されている(race テスト1件 + fake の etag 競合シミュレーション)

変更点

  • packages/shared/src/types/order.ts: settling 状態を追加。
    review_passed→settling→settled と失敗時 settling→review_passed を許可。
  • packages/functions/src/agents/settlement.ts: 送金前の原子的 claim、失敗時の claim 解放。
  • packages/functions/src/lib/cosmos-fake.ts: 同状態への遷移を etag conflict として
    拒否(実 Cosmos の IfMatch 412 を模す)。
  • packages/functions/src/agents/settlement.test.ts: 並走レーステスト追加。

これでピッチ Q&A の「冪等性キーで二重送金は構造的に不可能」が実装で担保されます。

…claim

dogfood #1 で二重送金を観測(1000 JPYC × 2、block 40122642/643)。原因は
Settlement の冪等性ガードが read-then-act(TOCTOU)で、ローカル駆動の Settlement と
closed(merged) webhook の Settlement が ~1.5s 並走し、両者が「未送金」を同時に読んで
両方とも transfer したため。

修正: 送金の直前に review_passed → settling を etag 楽観ロック(既存 transitionOrder
の IfMatch)で原子的に確保する。並走する2つの Settlement のうち claim に勝てるのは1つ
だけで、敗者は canTransition / etag 412 で送金前に弾かれる。これで「二重送金は構造的に
不可能」が実装で担保される(ピッチ Q&A の主張を事実に)。

- order.ts: settling 状態を追加。review_passed→settling→settled、失敗時 settling→
  review_passed ロールバックを許可。
- settlement.ts: 送金前の原子的 claim、転送失敗時の claim 解放。
- cosmos-fake.ts: 同状態への遷移を etag conflict として拒否(実 Cosmos の IfMatch 412
  を模す)。
- settlement.test.ts: 2つの Settlement が同一 order を並走しても transfer はちょうど
  1回、を検証するレーステスト追加(全31テスト通過)。
@Mameta29
Mameta29 merged commit ecd33ed into main Jun 14, 2026
1 check passed
@Mameta29

Copy link
Copy Markdown
Owner Author

💴 JPYC 送金完了 — Settled by Agentic Gig-Flow

項目
金額 1,000 JPYC
受取アドレス 0x7F37…9389
Tx Hash 0x3f540eef3fd90e8b35801149ad3bf80b8b7ba3d52185fc4a4aea4ebec745db33
ブロック 40123739
所要時間 (merge → 着金) 約 8.7 秒
Order ID: 1d26d357-d891-4aad-b656-01e445c6a3ab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settlement の二重送金を防ぐ原子的 claim 実装

1 participant