レッスン1 — Workspace + Reth + Malachite
問い
なぜ OpenHL の最初の1手は「機能実装」ではなく「依存固定」なのか?
原理(最小モデル)
OpenHL 初期構築の本質は、コードを書く前に再現性を固定すること。
- 再現性の単位は「crate 単体」ではなく workspace 全体
- Reth / Malachite は巨大依存なので、途中でぶつかると手戻りが大きい
- 本番系 L1 では semver 範囲指定より release-tag の commit SHA pin が優先される(バリデータ間でバイト単位の一致が要るため)
つまり最初のゴールは「機能」ではなく、依存グラフが安定して解決できる土台を作ること。
具体例
このレッスンで通すべき最小の完了条件は 1 つだけ。
cargo check --workspace
これが通る状態を先に作ると、後続レッスンは「依存調整」ではなく「設計そのもの」に集中できる。アプリケーションロジックは 1 行も書かない — それはレッスン 2 以降だ。
失敗例(誤解)
「まず actor や bridge を書き始めれば速い」は誤り。依存固定を後回しにすると、途中で Reth 系の transitive 衝突が出て、実装と環境調整が混ざり、原因の切り分けが難しくなる。
ここまでで「なぜ依存固定が初手か」は着地した。ここから先は、その原理を実際の workspace で組み立てる深掘りに入る。手を動かすパートは copy-paste で通る完全形にしてある。
🛑 考えてみよう。 スクロールする前に、root の
Cargo.tomlに書くmembersが何個になるか手元で書き出す。ヒント: ライブラリ crate 10 個 + binary crate 1 個。
ステップで組み立てる
前提
レッスン 0 の続き。手元には ~/code/my-openhl/(自分の workspace)と ~/code/openhl-reference/(psyto/openhl clone、答え合わせ専用)がある。編集は すべて my-openhl 側。各 stage は openhl の実 commit 75be9de → 5fc7ca1 に対応する(これは openhl 側 の commit で、下の Reth/Malachite の git rev とは別物)。
Step 1: workspace をリセット
cd ~/code/my-openhl
rm Cargo.toml Cargo.lock src/lib.rs
rmdir src
ls -la # .git だけが残る
Step 2: root Cargo.toml(依存はまだ Reth/Malachite 抜き)
[workspace]
resolver = "3"
members = [
"bin/openhl",
"crates/types", "crates/codec", "crates/clob", "crates/oracle",
"crates/funding", "crates/liquidation", "crates/vault",
"crates/evm", "crates/consensus", "crates/node",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.95"
license = "MIT OR Apache-2.0"
[workspace.dependencies]
# 内部 crate(path 依存)— 後続レッスンで相互参照する
openhl-types = { path = "crates/types" }
openhl-codec = { path = "crates/codec" }
openhl-clob = { path = "crates/clob" }
openhl-evm = { path = "crates/evm" }
openhl-consensus = { path = "crates/consensus" }
# (oracle/funding/liquidation/vault/node も同様に path 宣言)
# 共通ユーティリティ
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
thiserror = "1"
eyre = "0.6"
tracing = "0.1"
# --- Reth / Malachite は Step 7 / Step 8 でここに追加 ---
[workspace.lints.rust]
unsafe_code = "forbid"
本質的な選択が 3 つ(深掘りはここ。理由は 1 行ずつ):
resolver = "3"— feature unification を厳格化。Reth/Malachite の複雑な feature flag が後で衝突するのを防ぐ。unsafe_code = "forbid"(workspace 全体) — アプリ層でunsafeを禁止。pure state-machine がunsafeを欲しがった瞬間が review の警告サイン。これが本コースの determinism ルール。[workspace.dependencies]に一元宣言 — 各 crate は{ workspace = true }で継承する。Reth の version bump が 11 crate スイープではなく root 1 行で済む。(このブロックを省くと、Step 4 の crate 側{ workspace = true }が継承先を失いcargo checkが落ちる。)
Step 3: rust-toolchain.toml(root に置く)
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt"]
profile = "minimal"
これがないとマシンごとに違う rustc で別アーティファクトが出る(determinism risk)。
Step 4: crate テンプレートを 1 つ作る → 残り 9 つに展開
まず crates/types を完全な形で作る:
mkdir -p crates/types/src
crates/types/Cargo.toml:
[package]
name = "openhl-types"
version = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
license = { workspace = true }
[dependencies]
serde = { workspace = true }
[lints]
workspace = true
crates/types/src/lib.rs:
//! Shared primitives and CL/EL contract types.
残り 9 crate は name と doc comment を変えるだけ(mkdir -p crates/<name>/src → 上の 2 ファイルを配置)。[dependencies] は最初は空でよい(実依存は使うレッスンで足す)。
| crate | name | lib.rs doc |
|---|---|---|
| codec | openhl-codec | //! Canonical encoding for consensus messages. |
| clob | openhl-clob | //! CLOB matching engine — pure state machine. |
| oracle | openhl-oracle | //! Mark price aggregation. |
| funding | openhl-funding | //! Funding-rate calculation and settlement. |
| liquidation | openhl-liquidation | //! Liquidation engine. |
| vault | openhl-vault | //! Protocol-native vault primitive. |
| evm | openhl-evm | //! EVM execution layer — Reth integration. |
| consensus | openhl-consensus | //! Consensus layer — Malachite BFT. |
| node | openhl-node | //! Node assembly: consensus + evm + clob. |
Step 5: bin/openhl
mkdir -p bin/openhl/src
bin/openhl/Cargo.toml:
[package]
name = "openhl"
version = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
license = { workspace = true }
[[bin]]
name = "openhl"
path = "src/main.rs"
[dependencies]
[lints]
workspace = true
bin/openhl/src/main.rs:
fn main() {
println!("openhl v{}", env!("CARGO_PKG_VERSION"));
}
Step 6: 依存ゼロで最初の cargo check
cargo check --workspace # 期待: "Finished"(unused-dep 警告は正常)
落ちる主因: members の crate 名タイプミス / src/lib.rs 欠落 / [lints] に workspace = true 抜け。全部潰してから次へ。
Step 7: Reth を pin(v2.2.0 release-tag SHA — Stage 75be9de)
root の [workspace.dependencies] の # --- Reth / Malachite ... 行を、次で置き換える(rev は実 SHA、copy-paste 可):
# --- Reth (v2.2.0 release tag に pin。main HEAD には絶対 pin しない) ---
reth-node-builder = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
# 後続レッスンで使う reth-* はここに追記していく(全て同一 rev で揃える)
reth-primitives-traits = "0.3"
alloy-primitives = { version = "1.5", default-features = false }
alloy-consensus = { version = "2.0", default-features = false }
なぜ release-tag SHA か: version = "2.2" や branch pin は Reth が無関係な変更を出した瞬間に壊れうる。テスト済みの tag SHA に固定すれば安定ターゲットになる。*全 reth- を同一 rev で揃える**のが鉄則(混在は transitive 衝突の元)。
cargo check --workspace # 初回は Reth の transitive ~600 crate fetch で 5-15 分
ここはまだ resolution だけ(どの crate も Reth を use していない)。Finished になれば OK。
Step 8: Malachite を pin(v0.5.0 — Stage 5fc7ca1)
[workspace.dependencies] の末尾に追加(crate 名は informalsystems-malachitebft-* prefix、rev は実 SHA):
# --- Malachite BFT (v0.5.0 release tag に pin) ---
informalsystems-malachitebft-core-types = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
informalsystems-malachitebft-core-consensus = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
informalsystems-malachitebft-engine = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
informalsystems-malachitebft-app-channel = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
informalsystems-malachitebft-codec = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
informalsystems-malachitebft-signing-ed25519 = { git = "https://github.com/informalsystems/malachite", rev = "9ef02b33c4ded5fe3e072631d86448658680fe55" }
Rust ソースでは snake_case 化(informalsystems_malachitebft_core_types::Context)で参照する。
Step 9: 最終検証
cargo check --workspace # 期待: "Finished"
cargo run --bin openhl # 期待: "openhl v0.1.0"
答え合わせ(reference SHA との照合)
cd ~/code/openhl-reference && git checkout 5fc7ca1
diff -ru ~/code/my-openhl/Cargo.toml ./Cargo.toml
差分が whitespace / コメント程度なら OK。members の集合・両 rev pin・resolver が一致していれば依存グラフは正しい。意味のある差分が出たら設計判断のどこかがズレている — Step 2/7/8 を読み直す。
合格基準
cargo check --workspaceがFinishedcargo run --bin openhlがopenhl v0.1.0membersが 10 crate + 1 bin、Reth/Malachite が同一 rev pin(semver 範囲ではない)- reference の
5fc7ca1とCargo.tomlが構造的に一致
まとめ(3行)
- OpenHL の初手は「機能追加」ではなく「再現性固定」。
- 依存は workspace で一元管理し、release-tag SHA pin で揺れを止める。
- 概念が着地してから実 workspace を組み立て、reference SHA で答え合わせする。