FABRKNT
Step 1. Consensus — cargo init から single-validator devnet 構築
Foundations
レッスン 2 / 16·CONTENT45 分80 XP
コース
Step 1. Consensus — cargo init から single-validator devnet 構築
レッスンの役割
CONTENT
順序
2 / 16

レッスン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 75be9de5fc7ca1 に対応する(これは 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 行ずつ):

  1. resolver = "3" — feature unification を厳格化。Reth/Malachite の複雑な feature flag が後で衝突するのを防ぐ。
  2. unsafe_code = "forbid"(workspace 全体) — アプリ層で unsafe を禁止。pure state-machine が unsafe を欲しがった瞬間が review の警告サイン。これが本コースの determinism ルール。
  3. [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] は最初は空でよい(実依存は使うレッスンで足す)。

cratenamelib.rs doc
codecopenhl-codec//! Canonical encoding for consensus messages.
clobopenhl-clob//! CLOB matching engine — pure state machine.
oracleopenhl-oracle//! Mark price aggregation.
fundingopenhl-funding//! Funding-rate calculation and settlement.
liquidationopenhl-liquidation//! Liquidation engine.
vaultopenhl-vault//! Protocol-native vault primitive.
evmopenhl-evm//! EVM execution layer — Reth integration.
consensusopenhl-consensus//! Consensus layer — Malachite BFT.
nodeopenhl-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 --workspaceFinished
  • cargo run --bin openhlopenhl v0.1.0
  • members が 10 crate + 1 bin、Reth/Malachite が同一 rev pin(semver 範囲ではない)
  • reference の 5fc7ca1Cargo.toml が構造的に一致

まとめ(3行)

  • OpenHL の初手は「機能追加」ではなく「再現性固定」。
  • 依存は workspace で一元管理し、release-tag SHA pin で揺れを止める。
  • 概念が着地してから実 workspace を組み立て、reference SHA で答え合わせする。