FABRKNT
Step 4. Funding:決定論的数学パイプラインと Funding ステートマシンの構築
純粋な compute
レッスン 6 / 12·CONTENT30 分60 XP
コース
Step 4. Funding:決定論的数学パイプラインと Funding ステートマシンの構築
レッスンの役割
CONTENT
順序
6 / 12

レッスン5 — Overflow 哲学 + 最初の proptest

問い

compute_premium の積が i128 を超えたら何が起きるべきか? panic? wrap? それとも別の何か? consensus システムでは「validator ごとに違う答え」も「1 台だけ halt」も許されない。唯一許される overflow 挙動は何で、それをどうテストするか?

原理(最小モデル)

  • consensus で許される overflow は saturate だけ。 panic → validator が halt し network から fork off(liveness 喪失)。wrap → コンパイラ次第で「定義されているが誤った」値 → 誤値 fork or silent corruption。saturate → 全 validator が同じ bounded 値に合意。
  • 符号を意識した saturation の override。 i64::try_from は失敗を報告するが方向を教えない。unwrap_or(if v>0 {MAX} else {MIN}) のインライン条件式が方向を復元(固定 i64::MAX だと i128::MIN が正に flip して符号が壊れる)。
  • 手書きトレースと proptest は補完。 proptest のランダムサンプリングは i128::MAX(2^129 通りの 1 点)にまず当たらない。境界は手書きでしか pin できない。proptest は interior、手書きは corner。
  • テストすべきは実際に成立する不変条件、願望でない。 素朴な antisymmetry は magnitude も等しくと書きたくなるが整数除算が壊す。だから weaker な「符号が逆」をテストし、丸めの caveat はコメントに残す。
  • checked_mul + Result は解決にならない。 エラーは最終的に bridge に届き、bridge の選択肢は revert(fork) / skip(silent inconsistency) / cap で settle の 3 つ。最後のものは saturate がそのまま実現する。

具体例

「整数が収まらなかった」の失敗モードを validator から見た帰結で並べる:

モードRust 上の挙動network への影響判定
Panic (* debug)スレッド haltvalidator 1 台が consensus から永久脱落、network は気づかず前進❌ 自ら fork off(liveness 喪失)
Wrap (* release)silent に modulo wrap最適化次第で各 validator が別々の誤値、または全員一致で誤値合意❌ 検出不能な fork or silent corruption
Saturate (saturating_mul)i128::MAX/MIN に clamp全 validator が同じ bounded 値で合意・前進、経済的には capped settlement⭕ liveness 維持 — 唯一の選択肢

失敗例(誤解)

「境界(i128::MAX 等)は proptest がカバーするから手書きテスト不要」は誤り — proptest のデフォルト戦略は入力空間に uniform にサンプルし、i128::MAX は 2^129 通りの 1 点なのでランダムに当たる確率は実質ゼロ。境界には generator のランダムウォークが届かない特定値を狙い撃つ手書きトレースが要る。


ここまでで「saturate が唯一の consensus-safe な overflow」は着地した。ここから新関数なしで proptest を 1 つ足す(production コード変更なし — メンタルモデルが本体)。コードは完全形。

🛑 予測。 compute_premium で panic が起きれば validator は halt する。なぜそれが単一ノード障害でなく chain fork になるか?(答え: 他の validator は halt したノードを置き去りに前進する。再起動時には chain head が何ブロックも先で、halt したブロックでの local state が network の view と食い違い sync できない — 自ら network から fork off した形。saturate は validator を lockstep のまま保つ。)

ステップで組み立てる

Step 1: テストモジュールに proptest サポートを追加

compute.rs のテストモジュール冒頭をこう拡張:

#[cfg(test)]
mod tests {
    use super::*;
    use openhl_clob::AccountId;
    use proptest::prelude::*;

    fn pos(account: u64, size: i64) -> Position {
        Position {
            account: AccountId(account),
            size: crate::types::PositionSize(size),
        }
    }

    // ... レッスン4 の 4 unit test ...
}

use openhl_clob::AccountIdpos helper はレッスン7 の apply_funding テストで使うが、テストモジュールの import を安定化させるため今入れる。use proptest::prelude::*proptest!/prop_assert_eq!/strategy combinator を scope に持ち込む。

Step 2: Antisymmetry proptest を追加

4 unit test の後、閉じ } の前に:

    proptest! {
        /// Premium symmetry: swapping mark and index flips the sign.
        /// (Up to integer division rounding, the magnitude is the same — we
        /// allow off-by-one to absorb the rounding-toward-zero asymmetry.)
        #[test]
        fn premium_is_antisymmetric_in_mark_index(
            mark in 1u64..1_000_000,
            index in 1u64..1_000_000,
        ) {
            let a = compute_premium(MarkPrice(mark), IndexPrice(index));
            let b = compute_premium(MarkPrice(index), IndexPrice(mark));
            // Cross-multiplied magnitudes must be equal: |a| / mark == |b| / index
            // (i.e., the proportional dislocation is the same both ways).
            // We test the weaker property that the signs are opposite (or both zero).
            if mark == index {
                prop_assert_eq!(a, Premium(0));
                prop_assert_eq!(b, Premium(0));
            } else {
                prop_assert!(a.0.signum() == -b.0.signum());
            }
        }
    }

signum() は符号を -1/0/+1 で返す。mark in 1u64..1_000_000 が戦略(デフォルト 256 ケース)。素朴な antisymmetry(magnitude も等しい)は整数除算の丸めで破れるので、weaker な「符号が逆 or 両方ゼロ」をテストし、丸めの caveat をコメントに残す。0 を除外するのは index==0 がレッスン4 でカバー済みかつ「両方ゼロで符号反対」が破れるから。

Step 3: テスト実行

cargo test -p openhl-funding
running 5 tests
test compute::tests::premium_is_antisymmetric_in_mark_index ... ok
... (レッスン4 の 4 つ) ...
test result: ok. 5 passed; ...

256 個のランダム (mark, index) ペアで antisymmetry が満たされる。PROPTEST_VERBOSE=1 で「passed 256 cases」や失敗時の「shrunk to ...」が見える。

答え合わせ

cd ~/code/openhl-reference && git checkout cd94137
diff -u ~/code/my-openhl/crates/funding/src/compute.rs ./crates/funding/src/compute.rs
git checkout main

compute.rs は compute_premium + helper + 4 premium テスト + antisymmetry proptest + テストモジュール imports/helper まで一致(compute_rate/apply_funding はレッスン6/7)。

合格基準

cargo test -p openhl-funding が 5 pass。よくあるエラー: use proptest::*::prelude::* でない)でマクロ未解決 / prop_assert_eq!assert_eq! と typo(shrink されない)/ mark==index を else に流して符号テスト失敗。

まとめ(3行)

  • consensus 上で bounded behavior を提供する overflow 挙動は saturate だけ(panic=halt fork、wrap=誤値 fork)。saturate_i128_to_i64unwrap_or は符号依存にする(固定値だと i128::MIN が正に flip)。
  • テストするのは実際に成立する不変条件(符号が逆)であって願望(magnitude も等しい)でない。整数除算の丸めで破れる property はコメントに caveat を残す。
  • 手書きトレースは境界(generator が届かない i128::MAX 等)を pin、proptest は内部 property(antisymmetry を 256 ケース)を pin。補完的で冗長でない。

次のレッスン(レッスン6)

compute_rate を追加(Premium + FundingParamsFundingRate)。設計判断 3 つ(divisor==0 で funding 無効化 / 割ってから clamp / rate_cap を絶対値 clamp)と unit test 5 つ。