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

レッスン4 — compute_premium — 最初の数学、最初のテスト

問い

(mark - index) / index を整数で計算したい。だが u64 の引き算は mark < index で underflow し、整数除算を先にすると 100% 未満の premium が全部 0 に丸まる。符号を保ちつつ精度も残すには、型と演算順をどう組むか?

原理(最小モデル)

  • 整数幅は入力でなく 中間値 のレンジで選ぶ。 mark/indexu64 だが (mark-index)*RATE_SCALE は最悪 ~1.8e28。i128 中間値は必須、しかも upcast を引き算の に入れて初めて符号が保たれる。
  • 割る前に掛ければ精度が残る。 (mark-index)/index を整数で先に計算すると 100% 未満の premium が全部 0 に丸まる。先に RATE_SCALE を掛けて分数を i128 マグニチュードに変換し、その後で割る。
  • u64 引き算が王道の符号バグ。 99 - 100 を u64 でやると u64::MAX 近くに wrap(小さな負でなく巨大な正)。i128::from(...) upcast で代数的に正しくなる。
  • oracle 欠損は graceful degradation。 index==0Premium(0) を返しエラーにしない(funding は bridge 経由で balance update に流れるので Err は無関係な payload まで tx 失敗にする)。早期 return がゼロ除算 panic も同時に防ぐ。
  • テストコメントは紙の上の数学そのもの。 // (101-100)*1e9/100 = 10_000_000 を assertion 横に書けば、将来の debugger は「作者を信じる」でなく「数式に照らして検証」できる。

具体例

compute_premium 内で型がどこで widen/saturate/narrow するか:

  MarkPrice(u64) ──► i128 ──┐
                            ▼
  IndexPrice(u64) ──► i128 ─► [ - 引き算 ] ──► diff (i128: 符号が安全に残る)
                                                  │
  RATE_SCALE(i64) ──► i128 ──► [ saturating_mul ] ─► scaled (i128, overflow を clamp)
                                                  │
  IndexPrice(u64) ──► i128 ──────────────► [ / 除算 ]  (※ index==0 は事前に弾く)
                                                  ▼
  Premium(pub i64) ◄──── [ saturate_i128_to_i64 ] ◄── premium (i128)

入力(MarkPrice/IndexPrice/RATE_SCALE)と出力(Premium)は narrow、中間値だけ意図的に i128 に widen。overflow は saturating_mul と最後の saturate_i128_to_i64 の 2 箇所で吸収(間の diff には不要)。「掛けてから割る」が scaled ラベルの位置に現れる。

失敗例(誤解)

(mark - index).saturating_mul(RATE_SCALE) / index を u64 で計算」は誤り — 引き算が問題。MarkPrice(99) - IndexPrice(100) を u64 でやると underflow し u64::MAX 近くに wrap(小さな負でなく巨大な正)。本来小さな負の premium が巨大な正になる。符号が肝心、符号付き演算が必須。


ここまでで「i128 中間値 + 掛けてから割る」は着地した。ここから実際の数学を持つ最初のレッスンに入る(これ以降、コード変更でアカウント間に wealth が静かに移りうる — 手書きトレースのテストが期待値を紙の数学に pin する)。コードは完全形。

🛑 予測。 (mark - index) * RATE_SCALE の最大サイズは(mark/index は u64、RATE_SCALE = 1e9)? どの型に収まる必要があるか。(答え: (u64::MAX - 1) * 1e9 ≈ 1.8e28i64::MAX ~9.2e18 なので中間値に i128 必須。index で割った後は i64 に戻るが、除算は乗算の なので中間が i128 に収まることが必須。積に i128、saturation が最終結果の稀な i64 超えを扱う。)

ステップで組み立てる

Step 1: compute.rs を module doc 付きで作成

//! Pure funding-rate math.
//!
//! Three building blocks, each stateless:
//!   - [`compute_premium`] derives the mark/index gap as a signed fraction
//!   - [`compute_rate`] divides + caps to produce a per-interval rate
//!   - [`apply_funding`] turns a rate + position snapshot into settlements
//!
//! Each function is deterministic and saturates on overflow rather than
//! wrapping. Validators that disagree about funding fork the chain, so the
//! cost of an unexpected overflow has to be bounded behavior, not panic.

use crate::types::{
    FundingParams, FundingRate, IndexPrice, MarkPrice, Notional, Position, Premium, Settlement,
    RATE_SCALE,
};

module doc が 3 関数をプレビュー([compute_rate]/[apply_funding] はレッスン6/7 までリンク切れ、warning 許容)。use block はレッスン6/7 で使う型も今 import(boilerplate を先に安定化させ、レッスンごとの diff を本質に集中させる)。

Step 2: compute_premium を追加

/// Compute the premium `(mark - index) / index`, scaled by [`RATE_SCALE`].
///
/// Returns `Premium(0)` if `index == 0` — the safest behavior, since with no
/// reliable reference price the funding rate should not push capital around.
/// Real deployments should guard upstream (e.g., refuse to tick when the
/// oracle is missing); the saturation here is the second line of defense.
#[must_use]
pub fn compute_premium(mark: MarkPrice, index: IndexPrice) -> Premium {
    if index.0 == 0 {
        return Premium(0);
    }
    // (mark - index) as i128 so we can't lose sign on subtraction; multiply
    // by RATE_SCALE in i128 to avoid overflow before the divide.
    let diff = i128::from(mark.0) - i128::from(index.0);
    let scaled = diff.saturating_mul(i128::from(RATE_SCALE));
    let premium = scaled / i128::from(index.0);
    // Saturate back to i64 — at i64 range with index prices in u64::MAX
    // territory, this only clips at network-pathological inputs.
    Premium(saturate_i128_to_i64(premium))
}

動く部分 4 つ: index==0 早期 return(graceful degradation + 直後の scaled / index のゼロ除算 panic も同じ 2 行で封じる)/ 引き算の前に i128 upcast(u64 underflow 回避)/ saturating_mul(panic/wrap でなく i128::MAX/MIN に clamp)/ 割るのは掛けた後(精度保持)。

Step 3: saturate_i128_to_i64 helper を追加

/// Clamp an `i128` into the `i64` range. Used wherever an intermediate
/// product can exceed `i64::MAX` at network-pathological inputs (e.g., a
/// `u64::MAX` index price). Saturation, not wrapping — see the module-doc
/// comment on why panicking would be a worse failure mode.
fn saturate_i128_to_i64(v: i128) -> i64 {
    i64::try_from(v).unwrap_or(if v > 0 { i64::MAX } else { i64::MIN })
}

i64::try_from(v)Resultunwrap_or が overflow 方向に応じた default を返す(正なら i64::MAX、負なら i64::MIN)。module private(fn、呼び出し側は MarkPricePremium を見るだけ)。レッスン7 の apply_funding が 2 番目の caller — だから inline でなく独立 helper にする。

Step 4: テストモジュール + 4 unit test

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn premium_zero_when_mark_equals_index() {
        let p = compute_premium(MarkPrice(100), IndexPrice(100));
        assert_eq!(p, Premium(0));
    }

    #[test]
    fn premium_positive_when_mark_above_index() {
        // mark 101, index 100 → premium = 1/100 = 0.01 → 10_000_000 ppb
        let p = compute_premium(MarkPrice(101), IndexPrice(100));
        assert_eq!(p, Premium(10_000_000));
    }

    #[test]
    fn premium_negative_when_mark_below_index() {
        let p = compute_premium(MarkPrice(99), IndexPrice(100));
        assert_eq!(p, Premium(-10_000_000));
    }

    #[test]
    fn premium_saturates_to_zero_when_index_is_zero() {
        let p = compute_premium(MarkPrice(1_000_000), IndexPrice(0));
        assert_eq!(p, Premium(0));
    }
}

手書きトレース 4 つ(equal→0 / positive→longs overpay / negative→u64 underflow バグを捕まえる / index-zero→graceful guard)。コメント // (101-100)*1e9/100 = 10_000_000 が紙の数学。pathological 入力(u64::MAX 等)の境界テストはレッスン5。

Step 5: lib.rs を更新

pub mod compute;
pub mod types;

pub use compute::compute_premium;
pub use types::{
    FundingParams, FundingRate, IndexPrice, MarkPrice, Notional, Position, PositionSize,
    Premium, Settlement, RATE_SCALE,
};

モジュール宣言・re-export ともアルファベット順(computetypes の前)。

Step 6: テスト実行

cargo test -p openhl-funding
warning: unresolved link to `compute_rate`
warning: unresolved link to `apply_funding`
warning: unresolved link to `FundingClock`
running 4 tests
test compute::tests::premium_negative_when_mark_below_index ... ok
test compute::tests::premium_positive_when_mark_above_index ... ok
test compute::tests::premium_saturates_to_zero_when_index_is_zero ... ok
test compute::tests::premium_zero_when_mark_equals_index ... ok
test result: ok. 4 passed; 0 failed; ...

crate 初の green run。rustdoc warning 3 つは期待通り(レッスン6/7/8 で解決)。

答え合わせ

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

compute.rs は compute_premium + saturate_i128_to_i64 + 4 premium テストまで一致(compute_rate/apply_funding/proptest はレッスン5-7)。

合格基準

cargo test -p openhl-funding が 4 pass。よくあるエラー: * RATE_SCALE 抜けで left=0(整数除算が先に 0 に丸める)/ i128 upcast 抜けで left=18446744073709541616(u64 underflow wrap)/ saturating_mul でなく * で panic(debug overflow)/ helper の宣言順は Rust が気にしない。

まとめ(3行)

  • compute_premium(mark-index)/indexRATE_SCALE スケールで返す。中間値を i128 に widen(入力は narrow なまま)し、引き算の前に upcast して符号を保つ。
  • 「掛けてから割る」で精度を残し、overflow は saturating_mul + saturate_i128_to_i64 で吸収(panic/wrap は consensus を壊すので不可)。
  • index==0Premium(0) で graceful degradation(兼ゼロ除算 panic 回避)。テストコメントが紙の数学で、debugger が assertion を数式に照らせる。

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

新関数なし。overflow 哲学を深掘り(consensus で許される overflow は saturate だけ — panic は halt 経由 fork、wrap は誤値 fork)し、crate 初の proptest premium_is_antisymmetric_in_mark_index を追加する。