レッスン1 — RATE_SCALE — consensus を守る定数
問い
funding の全計算(premium / rate / settlement)を float なしで、全 validator が bit-exact に一致させたい。何を基準単位に選び、それを i64 のどこに置けば精度とヘッドルームを両立できるか?
原理(最小モデル)
- float は consensus で動かない。 FMA・丸めモード・denormal の扱いはコンパイラ/CPU ごとに違うビットを生み、rate の 1 LSB ズレが chain fork に直結する。
RATE_SCALE = 1e9(parts-per-billion)が i64 のスイートスポット。 9 桁精度 + 積の中間値でも i64 まで 11 桁ヘッドルーム。1e6は精度不足(10 ppb が 0 に丸まる)、1e12はヘッドルーム不足(積に i256 が要る)。i64、u64でない。 rate/premium は符号付き(longs 支払い=正、shorts 支払い=負)。符号付きなら compute の符号チェックが不要、i128中間値が積を吸収する。RATE_SCALEは consensus 定数であって調整パラメータでない。 デプロイ後は immutable(全 balance / 全 settlement / 全 fixture がこれ前提に calibrate)。crate 開始時に一度constで設定する。
具体例
ヘッドルームを桁の物差しに並べる:
桁 値 何が住んでいるか
─────────────────────────────────────────────────────────────────────────────
1e18 ──────── 9_223_372_036_854_775_807 ─────── i64::MAX (約 920 京)
1e18 ↑ 中間値の天井
1e15 ──────── 1_600_000_000_000_000 ─────── 4% × 4% (キャップ²) = 1.6e15
1e9 ──────── 1_000_000_000 ─────── RATE_SCALE = 100% (10 億)
1e7 ──────── 40_000_000 ─────── HL Funding Cap 4% (4 千万)
1e6 ──────── 1_000_000 ─────── 0.1%
1e1 ──────── 10 ─────── 0.0001% = 10 ppb (現実的な最小粒度)
キャップ 4e7 と RATE_SCALE 1e9 の間に 1 桁以上、キャップ²でも 1.6e15 で i64 天井 9.2e18 まで 3 桁 — 「rate × notional」程度の積は i128 で吸収し、最後に RATE_SCALE で割って i64 に戻せる。最小粒度 10 ppb(0.0001%)も表現できる(1e6 だと 0 に丸まる)。
失敗例(誤解)
「f64 で計算して validator 間共有の前に N 桁に丸めればいい」は誤り、2 つの理由: (1) 中間計算が最終丸めより先に発散し、その時点で被害が出ている (2)「N 桁に丸める」自体が float 演算で処理系ごとに挙動が違う。float 非決定性からの脱出口は整数より単純なものはない。
ここまでで「なぜ 1e9 の整数か」は着地した。ここから crate を「public な値を 1 つ持つ実 crate」に変える(編集 3 つ、テストなし — RATE_SCALE は値であって挙動でない。最初のテストはレッスン2)。コードは完全形。
🛑 予測。
RATE_SCALEを1e6(ppm)や1e12(ppt)でなく1e9にする理由は?(答え: i64 max ~9.2e18。1e9なら 9 桁精度 + キャップ4e7から天井まで 11 桁ヘッドルーム。1e12は精度↑だが積に i256 が要る。1e6は 10 ppb が 0 に丸まり精度不足。1e9が i64 固定小数点 rate のスイートスポット。)
ステップで組み立てる
Step 1: Cargo.toml を更新
crates/funding/Cargo.toml の [dependencies] 以下をこうする:
[dependencies]
openhl-clob = { path = "../clob" }
[dev-dependencies]
proptest = { workspace = true }
[lints]
workspace = true
openhl-clob(AccountId がレッスン3 の Position で要る — production の型シグネチャの一部なので dev-dep でなく通常 dep)+ [dev-dependencies] proptest(レッスン4/7 で使う)。今宣言して Cargo.toml の diff を 1 箇所に集中させる(path dep は最初の use まで recompile を走らせないのでコストほぼゼロ)。
Step 2: src/types.rs を作成
新規ファイル。module doc と RATE_SCALE のみ:
//! Core types for the funding state machine.
//!
//! Pure data — no I/O, no allocation beyond what's needed for settlements.
//! Every type is `Copy`-friendly (or, in the case of `Position`, `Clone +
//! Copy`) so callers can pass snapshots without lifetime gymnastics.
//!
//! ### Why fixed-point integers, not floats
//!
//! Consensus determinism — every validator must compute the *same* funding
//! rate from the *same* inputs. Float arithmetic gives different bit patterns
//! across compilers and CPUs (FMA, rounding mode, denormal handling); the
//! moment two validators disagree on a single LSB they fork. We use signed
//! integers scaled by [`RATE_SCALE`] (parts-per-billion) for rates and
//! premiums, and a separate `Notional` type for quote-currency deltas.
/// Scale factor for [`FundingRate`] and [`Premium`]. A raw value of
/// `RATE_SCALE` represents `1.0` (i.e., 100%). With `1e9` we get 9 decimal
/// digits of precision — more than enough for funding rates that typically
/// sit in the ±0.01% to ±0.05% per interval band.
pub const RATE_SCALE: i64 = 1_000_000_000;
module doc に「Why fixed-point integers, not floats」を置く — crate 全体の load-bearing な理由付けで、コミットメッセージに埋もれさせず最上部に置く。[FundingRate]/[Premium] クロス参照はまだリンク切れ(レッスン2/3 で解決、warning は受け入れる)。i64(u64 でない)— rate/premium は符号付き。
Step 3: src/lib.rs を更新
空だった lib.rs をこう置き換える:
//! `openhl-funding` — funding-rate state machine.
//!
//! Pure state machine: no I/O, no async, no networking. Funding is applied
//! deterministically on a fixed cadence (see [`FundingClock`]); every tick is
//! a pure function over `(now, mark, index, positions)` → settlements.
//!
//! ### Hyperliquid-shape funding, in one paragraph
//!
//! Perpetual contracts don't expire, so the mark price can drift arbitrarily
//! from the spot ("index") price. Funding payments push it back: when mark >
//! index (longs are overpaying), longs pay shorts; when mark < index, shorts
//! pay longs. The premium `(mark - index) / index` is divided by a
//! per-day-interval count (HL: divisor 8 — one settlement every 1 hour, scaled to a daily rate) to derive a
//! per-interval rate, capped at a network-set absolute max. At each tick
//! every account with an open position settles `position_size * mark * rate`
//! in quote currency.
//!
//! Integration with the rest of openhl happens at the EVM bridge: settlement
//! deltas become balance updates that the bridge bundles into payloads. That
//! integration lives in `crates/evm/`; the rate math and tick gating are here.
pub mod types;
pub use types::RATE_SCALE;
pub mod 宣言は対応ファイルを作るタイミングで足す(pub mod compute; を書いて compute.rs がないと error[E0583])。pub use types::RATE_SCALE で呼び出し側は openhl_funding::RATE_SCALE の短いパスを使える。
Step 4: コンパイル
cargo build -p openhl-funding
warning: unresolved link to `FundingRate`
warning: unresolved link to `Premium`
warning: unresolved link to `FundingClock`
Finished `dev` profile [unoptimized + debuginfo] in 0.5s
rustdoc warning 3 つは期待通り(リンク先はレッスン2/3/8 で順次追加)。#[allow(rustdoc::broken_intra_doc_links)] で抑制しない — 「まだ X を足す必要がある」インジケータとして有用。
答え合わせ
cd ~/code/openhl-reference && git checkout cd94137
diff -u ~/code/my-openhl/crates/funding/Cargo.toml ./crates/funding/Cargo.toml
diff -u ~/code/my-openhl/crates/funding/src/types.rs ./crates/funding/src/types.rs
diff -u ~/code/my-openhl/crates/funding/src/lib.rs ./crates/funding/src/lib.rs
git checkout main
Cargo.toml は cd94137 と完全一致、types.rs は最初の ~30 行(module doc + RATE_SCALE)まで一致、lib.rs は短い版(pub mod types; + re-export 1 つ)。
合格基準
cargo build -p openhl-funding が通り、rustdoc warning 3 つ(unresolved link、期待通り、抑制しない)。よくあるエラー: E0463(openhl-clob 行を忘れ)/ E0583(pub mod clock/compute を先取り)/ manifest parse([dev-dependencies] の typo)。
まとめ(3行)
RATE_SCALE = 1_000_000_000(parts-per-billion)— funding の全計算が乗る基準単位。float は 1 LSB のズレで chain fork を招くので使わず、すべて符号付き整数で bit-exact に計算する。1e9は i64 のスイートスポット: 9 桁精度 + キャップ4e7から天井まで 11 桁ヘッドルーム(i128中間値で積を吸収して最後に割り戻せる)。- consensus 定数なのでデプロイ後は immutable。
i64(符号付き)で crate 開始時に一度const設定。テストなし(値であって挙動でない)。
次のレッスン(レッスン2)
money type を 4 つ追加する(MarkPrice/IndexPrice/Premium/Notional)。焦点は「なぜ固定小数点」から「なぜ newtype」へ — MarkPrice を期待する箇所に IndexPrice を渡す引数順バグをコンパイル時に潰す。