FABRKNT
Step 2. CLOB — マッチングエンジンの追加とステートマシンの統合
CLOB 型
レッスン 2 / 13·CONTENT25 分60 XP
コース
Step 2. CLOB — マッチングエンジンの追加とステートマシンの統合
レッスンの役割
CONTENT
順序
2 / 13

レッスン1 — CLOB の newtype、SideOrderType

問い

CLOB の基本型をどう設計すれば、引数の取り違えバグを「実行時に静かに誤計上される問題」から「コンパイルエラー」へ格上げできるか? そして金銭計算をどう厳密整数に保つか?

原理(最小モデル)

  • 型安全性としての newtype。 u64AccountId / OrderId / Price / Qty で包む。(u64, u64, u64) を取る関数は引数を間違った順序で呼んでもコンパイルが通るが、newtype なら compiler が拒否する。
  • 金銭は整数のみ。 Price / Qtyu64 ベース、f64 禁止。float 中間値が紛れ込めば engine の厳密整数 invariant(例「約定は数量を保存する」)が一発で壊れる。
  • struct スタイルの enum variant。 OrderType::Limit { price }Limit(Price) より、全 pattern match 箇所で意図が読める(位置でなく field に名前がある)。
  • field-level 型を先に確定。 atomic な型はここで固め、record 型(Order / Fill)はレッスン2 でその上に積む。

具体例

submit(book, account: u64, price: u64, qty: u64)   // 引数を取り違えても compile が通る ✗
submit(book, AccountId, Price, Qty)                // 間違った型は compile error ✓

失敗例(誤解)

「便利だから Price::from_dollars(f64) を足す」は誤り — f64 の精度問題を engine に持ち込む。表示用の dollar 変換はツール側の境界でやり、engine には integer-typed Price を渡す。「side を bool で」も誤り — submit_order(order, true) は call site で意味が消える(Side::Buy なら一目瞭然)。


ここまでで「なぜ newtype + integer + struct enum か」は着地した。ここから crates/clob/ を作る。コードは完全形。

🛑 予測。 同じ u64 を wrap する newtype 4 個(AccountId/OrderId/Price/Qty)が各々防ぐ 1 つのバグ は何か? ヒント: (u64, u64, u64) を取る関数を、誰かが引数を間違った順序で呼ぶ場面。

ステップで組み立てる

Step 1: crate ディレクトリ + Cargo.toml

mkdir -p crates/clob/src
touch crates/clob/Cargo.toml crates/clob/src/lib.rs crates/clob/src/types.rs

crates/clob/Cargo.toml:

[package]
name         = "openhl-clob"
version      = { workspace = true }
edition      = { workspace = true }
rust-version = { workspace = true }
license      = { workspace = true }
repository   = { workspace = true }
authors      = { workspace = true }

[lints]
workspace = true

依存なし(pure data + pure logic、この段階では serde も不要)。

Step 2: workspace に登録

root Cargo.toml[workspace] members に追加し、[workspace.dependencies] に path エントリを追加:

[workspace]
resolver = "3"
members = [
    "bin/openhl",
    "crates/types",
    "crates/clob",      # NEW
    "crates/evm",
    "crates/consensus",
]

[workspace.dependencies]
# --- Internal crates ---
openhl-types     = { path = "crates/types" }
openhl-clob      = { path = "crates/clob" }     # NEW
openhl-evm       = { path = "crates/evm" }
openhl-consensus = { path = "crates/consensus" }

これで bridge(レッスン9)が openhl-clob = { workspace = true } で consume できる。

Step 3: newtype 4 個

crates/clob/src/types.rs:

//! Core types for the CLOB matching engine.
//!
//! Pure data — no I/O, no allocation beyond what's needed for fills. The
//! whole module is deterministic by construction: every type's `PartialEq`
//! and `Ord` impl derives from byte-equal field comparison.

use core::fmt;

/// Account identifier. Opaque to the CLOB; chain integration maps these to
/// EVM addresses, validator addresses, or whatever the chain uses.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AccountId(pub u64);

/// Sequential order identifier. Caller allocates; the book doesn't generate.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OrderId(pub u64);

/// Price in minor units. For a USDC market, `Price(1_000_000) = $1.00`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Price(pub u64);

/// Quantity in minor units.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Qty(pub u64);

7 個の derive は 4 型で同一(意図的)— newtype は u64 と同じ操作を持ちつつ型システムが混在を拒否する。要点: AccountId は opaque(CLOB は chain が何を使うか知らず equality 比較のみ)、OrderId は caller-allocated(book を pure-stateless に保つ)、Price/Qty は minor unit(engine 内に f64 は存在しない)。

Step 4: Side + opposite()

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Side {
    Buy,
    Sell,
}

impl Side {
    #[must_use]
    pub const fn opposite(self) -> Self {
        match self {
            Self::Buy => Self::Sell,
            Self::Sell => Self::Buy,
        }
    }
}

opposite() は後で load-bearing — taker は book の 反対側 を辿って流動性を探す(Buy taker は ask、Sell taker は bid)。ルールをここに 1 度だけ encode。PartialOrd/Ord付けない のは意図的(「Buy < Sell」は無意味で、declaration 順の偶発的順序付けを防ぐ)。

Step 5: OrderType

/// Order type — describes liquidity-taking + liquidity-providing behavior.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OrderType {
    /// Take liquidity at or better than `price`; rest the remainder on the book.
    Limit { price: Price },
    /// Take whatever liquidity is available at any price; never rests.
    Market,
}

Limit { price } は at-or-better でマッチし残りは book に rest。Market は価格なし、残りは破棄。struct スタイルにするのは、match 時に field 名 price がパターンに入って self-documenting になるから。未使用 variant(Stop 等)は型を使う直前まで追加しない。

Step 6: Display(user-facing な 3 型)

impl fmt::Display for OrderId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{}", self.0)
    }
}

impl fmt::Display for Price {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Display for Qty {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

AccountId に Display を 付けない(opaque ID なので、生 u64 でなく chain 統合のマッピングが返す実アドレスを print したいはず → caller に明示的扱いを強制)。

Step 7: lib.rs

//! Pure-Rust CLOB (central limit order book) matching engine for openhl.
//!
//! No I/O. No allocation beyond fill output. Deterministic by construction.
//! See [`book::Book`] for the matching state machine (レッスン 3+).

pub mod types;

pub use types::*;

pub use types::* で crate ルート re-export → caller は use openhl_clob::{Order, Side} と短く書ける。book モジュールはレッスン3 で追加。

答え合わせ

cd ~/code/openhl-reference && git checkout 55a9dff
diff -u ~/code/my-openhl/crates/clob/src/types.rs ./crates/clob/src/types.rs
git checkout main

参照の types.rs は全型で ~109 行。本レッスン後は newtype + Side + OrderType + Display のみ(~65 行)。残り ~45 行(Order/Fill/FillResult)はレッスン2。

合格基準

cargo check -p openhl-clob
cargo check --workspace

→ 警告・エラーなし。public API は AccountId/OrderId/Price/Qty/Side/OrderType。よくあるミス: use core::fmt; 忘れ / Price/Qty に Display 付け忘れ / lib.rsmod types;(private)。

まとめ(3行)

  • newtype で argument-swap バグを compile time に潰す(コストは .0 deref 数個)。
  • 金銭は integer のみ(Price/Qtyu64f64 を engine に持ち込まない)。
  • OrderType::Limit { price } の struct スタイルが、match を self-documenting にする。