レッスン5 — real alloy 型を使う RethEvmBridge
問い
レッスン4 と同じ ConsensusBridge を、合成型ではなく 本物の alloy / Reth 型(B256, Header)で満たすには? そして合成 hash と real hash の違いは、どんな挙動を testable にするか?
原理(最小モデル)
- 内部は alloy-native、trait 型は contract の serialization。 State は
(B256, Header)を保存し、trait はExecutedBlockを返す。変換は trait 境界でだけ。alloy が進化しても変換ヘルパーを直すだけで contract は壊れない。 Header::hash_slow()で real RLP hashing。 header 全体を RLP encode + Keccak-256。"slow" は「cache なし、毎回再計算」の命名 convention。Ethereum node が計算するのと同じ hash になる。(B256, Header)タプルで保持。 hash は ちょうどこの header の hash。別フィールドに分けると header 変更で cache hash が desync する。タプルが両者を不可分にする。- 1 trait・2 impl。
InMemoryEvmBridgeとRethEvmBridgeは trait surface を共有し fidelity だけ違う。レッスン11+ で 3 つ目(LiveRethEvmBridge)に広がる。
具体例
型境界のレイアウト:
[ 外側: CL 空間 ] openhl-types: BlockHash / PayloadId / ExecutedBlock
▲ │ trait boundary でだけ変換(to_b256 / from_b256 / to_executed_block)
│ ▼
[ 内側: EL 空間 ] alloy: B256 / u64 / Header
※ State は (B256, Header) タプル — hash と Header を常に同期させる
contract 型は 4 method の signature/戻り値にしか現れず、impl 内部は全て alloy 型。
失敗例(誤解)
「B256 も BlockHash も [u8;32] を wrap している。transmute で変換できる」は誤り。byte layout は同一でも型は別物 — それが point。変換関数が境界の場所を document する。将来 BlockHash が checksum 等の metadata を持てば transmute はバグになるが、to_b256 は更新すべき場所として残る。
ここまでで「内部 alloy / 境界変換・real hashing・タプル保持」は着地した。ここから engine.rs を組み立てる。コードは完全形。
🛑 予測。 レッスン4 は hash を
(id, number)から合成した。レッスン5 はheader.hash_slow()を呼ぶ。この違いで testable になる挙動は何か? ヒント: header の 1 フィールド(timestamp)を変えると hash はどうなるか。
ステップで組み立てる
Step 1: crates/evm/Cargo.toml に alloy 依存を追加
[dependencies]
openhl-consensus = { workspace = true }
openhl-types = { workspace = true }
async-trait = { workspace = true }
alloy-primitives = { workspace = true }
alloy-consensus = { workspace = true }
alloy-primitives が B256(32-byte hash)/ Address(20-byte)、alloy-consensus が Header(Ethereum block header 全フィールド)。
Step 2: crates/evm/src/engine.rs — doc + imports + struct
//! Reth-backed `ConsensusBridge` — uses alloy / Reth types throughout.
//!
//! At v0 this maintains state in-process for the parts that would normally
//! require a running Reth node (`PayloadBuilder` service, `BlockchainProvider`).
//! The live-node bootstrap lands in later lessons (レッスン 10〜13); the type
//! conversions and state-machine shape here are the contract that bootstrap
//! will satisfy.
use alloy_consensus::Header;
use alloy_primitives::{Address, B256};
use async_trait::async_trait;
use openhl_consensus::bridge::{BridgeError, ConsensusBridge};
use openhl_types::{BlockHash, ExecutedBlock, PayloadAttrs, PayloadId, PayloadStatus};
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Debug, Default)]
pub struct RethEvmBridge {
state: Mutex<State>,
}
#[derive(Debug, Default)]
struct State {
next_payload_id: u64,
pending: HashMap<u64, (B256, Header)>,
chain: HashMap<B256, Header>,
head: Option<B256>,
}
impl RethEvmBridge {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
レッスン4 と同じ shape だが State 内の型が違う: pending が (B256, Header)、chain/head の key が B256。(B256, Header) で保持するのは hash_slow() が expensive だから — insert 時に 1 度 hash を計算して cache する。
Step 3: build_payload — 初めての real hashing
#[async_trait]
impl ConsensusBridge for RethEvmBridge {
async fn build_payload(
&self,
parent: BlockHash,
attrs: PayloadAttrs,
) -> Result<PayloadId, BridgeError> {
let parent_hash = to_b256(parent);
let mut s = self.state.lock().expect("state mutex poisoned");
let parent_number = s.chain.get(&parent_hash).map_or(0, |h| h.number);
let id = s.next_payload_id;
s.next_payload_id += 1;
let header = Header {
parent_hash,
number: parent_number + 1,
timestamp: attrs.timestamp,
beneficiary: Address::from(attrs.fee_recipient),
mix_hash: B256::from(attrs.prev_randao),
..Default::default()
};
let hash = header.hash_slow();
s.pending.insert(id, (hash, header));
Ok(PayloadId(id))
}
4 フィールドだけ set し ..Default::default() で残り(state_root, gas_limit 等)を埋める。header.hash_slow() が本物の hash — header の 1 byte でも変われば hash が変わる(予測の答え: timestamp を変えると hash が変わる、合成 hash にはなかった性質)。
Step 4: payload_ready / validate_payload / commit
async fn payload_ready(&self, id: PayloadId) -> Result<ExecutedBlock, BridgeError> {
let s = self.state.lock().expect("state mutex poisoned");
let n = id.0;
let (hash, header) = s
.pending
.get(&n)
.cloned()
.ok_or_else(|| BridgeError::Rejected(format!("unknown payload id {n}")))?;
Ok(to_executed_block(hash, &header))
}
async fn validate_payload(
&self,
_block: &ExecutedBlock,
) -> Result<PayloadStatus, BridgeError> {
// Real validation requires a live Reth provider + EVM (lessons レッスン 11+).
// For now, defer to the CL's voting layer for actual block validity
// and accept structurally.
Ok(PayloadStatus::Valid)
}
async fn commit(&self, block_hash: BlockHash) -> Result<(), BridgeError> {
let hash = to_b256(block_hash);
let mut s = self.state.lock().expect("state mutex poisoned");
let header = s
.pending
.values()
.find(|(h, _)| *h == hash)
.map(|(_, header)| header.clone())
.ok_or_else(|| BridgeError::Rejected(format!("commit for unknown hash {hash}")))?;
s.chain.insert(hash, header);
s.head = Some(hash);
Ok(())
}
}
find(|(h, _)| *h == hash) はタプルを destructure して 1 番目を比較。*h は B256 が Copy なので memcpy が走るだけで所有権 move しない。
Step 5: 変換ヘルパー 3 つ(impl ブロックの後)
fn to_b256(h: BlockHash) -> B256 {
B256::from(h.0)
}
fn from_b256(b: B256) -> BlockHash {
BlockHash(b.0)
}
fn to_executed_block(hash: B256, header: &Header) -> ExecutedBlock {
ExecutedBlock {
hash: from_b256(hash),
parent_hash: from_b256(header.parent_hash),
number: header.number,
state_root: header.state_root.0,
}
}
3 つに分けるのは各々が 1 つのことだけをするから(to_b256/from_b256 は pure な型橋渡し、to_executed_block がフィールド mapping を知る)。
Step 6: crate に組み込む
crates/evm/src/lib.rs:
//! EVM execution layer — Reth integration.
pub mod engine;
pub mod in_memory;
pub use engine::RethEvmBridge;
pub use in_memory::InMemoryEvmBridge;
Step 7: 4 unit test(ファイル末尾)
#[cfg(test)]
mod tests {
use super::*;
fn attrs() -> PayloadAttrs {
PayloadAttrs {
timestamp: 42,
fee_recipient: [0xaa; 20],
prev_randao: [0xbb; 32],
}
}
#[tokio::test]
async fn build_then_ready_returns_alloy_hashed_block() {
let bridge = RethEvmBridge::new();
let parent = BlockHash([1u8; 32]);
let id = bridge.build_payload(parent, attrs()).await.unwrap();
let block = bridge.payload_ready(id).await.unwrap();
assert_eq!(block.parent_hash, parent);
assert_eq!(block.number, 1);
// Hash is computed by alloy_consensus::Header::hash_slow, not synthesized:
// it changes if any header field changes.
let mut alt_attrs = attrs();
alt_attrs.timestamp += 1;
let id2 = bridge.build_payload(parent, alt_attrs).await.unwrap();
let block2 = bridge.payload_ready(id2).await.unwrap();
assert_ne!(block.hash, block2.hash);
}
#[tokio::test]
async fn commit_advances_head() {
let bridge = RethEvmBridge::new();
let parent = BlockHash([1u8; 32]);
let id = bridge.build_payload(parent, attrs()).await.unwrap();
let block = bridge.payload_ready(id).await.unwrap();
bridge.commit(block.hash).await.unwrap();
let s = bridge.state.lock().unwrap();
assert_eq!(s.head, Some(to_b256(block.hash)));
}
#[tokio::test]
async fn build_on_committed_parent_increments_number() {
let bridge = RethEvmBridge::new();
let genesis = BlockHash([1u8; 32]);
let id1 = bridge.build_payload(genesis, attrs()).await.unwrap();
let block1 = bridge.payload_ready(id1).await.unwrap();
bridge.commit(block1.hash).await.unwrap();
let id2 = bridge.build_payload(block1.hash, attrs()).await.unwrap();
let block2 = bridge.payload_ready(id2).await.unwrap();
assert_eq!(block2.number, 2);
assert_eq!(block2.parent_hash, block1.hash);
}
#[tokio::test]
async fn commit_unknown_hash_errors() {
let bridge = RethEvmBridge::new();
let err = bridge.commit(BlockHash([9u8; 32])).await.unwrap_err();
assert!(matches!(err, BridgeError::Rejected(_)));
}
}
鍵は最初のテスト: 同じ parent でも timestamp を変えると hash が変わる — レッスン4 の合成 hash には書けなかったテスト(same parent + same number → same synthesized hash で timestamp を無視した)。
答え合わせ
cd ~/code/openhl-reference && git checkout c938321
diff -u ~/code/my-openhl/crates/evm/src/engine.rs ./crates/evm/src/engine.rs
git checkout main
doc/error message の variation は OK。struct 型・helper signature・4 method のロジックはほぼ一致。reference の c938321 の Cargo.toml には後のレッスン用に reth-ethereum-primitives も列挙されている(engine.rs では未使用、省略して OK)。
合格基準
cargo test -p openhl-evm
→ 9 テスト pass(レッスン4 の 5 + レッスン5 の 4)。両 impl が同じ trait を満たし、レッスン8/9 の同じ consumer コードがどちらにも動く。よくあるミス: hash_slow() の戻りを BlockHash に直代入(B256 なので from_b256)/ ..Default::default() 忘れ / fee_recipient([u8;20])に B256::from(正しくは Address::from)。
まとめ(3行)
- 内部は alloy-native(
B256/Header)、trait 型は境界での serialization — alloy 進化に強い。 header.hash_slow()で real RLP hashing(header 1 フィールド変更で hash が変わるのを test で証明)。- 1 trait・2 impl の多相性が、レッスン11+ の 3 つ目
LiveRethEvmBridgeへそのまま広がる。