FABRKNT
スタックを読む — 中級への橋渡し
ソース読みのための Rust
レッスン 5 / 10·CONTENT18 分35 XP
コース
スタックを読む — 中級への橋渡し
レッスンの役割
CONTENT
順序
5 / 10

レッスン4 — Solidity エンジニアのための Rust — 移行マップ

問い

Solidity を書いてきた人が Rust source(Reth / Revm / Alloy)を読むときの 移行マップ。Solidity の概念と Rust の対応 + 違い + 注意点。Solidity 経験が活きる場所と頭の切り替えが必要な場所を整理。

原理(最小モデル)

  • 型システム. Solidity の uint256 → Rust の U256(256 ビット)/ addressAddress(20 バイト)/ bytes32B256、Alloy primitive で 1:1 対応。
  • 所有権 = メモリ管理. Solidity は EVM が自動管理、Rust はコンパイル時所有権 + 借用 = GC なし + 二重解放防止 + データ競合防止。
  • Solidity の状態変数 = Rust の struct field. contract C { uint x; }struct C { x: U256 }、メソッドは impl C { fn ... }
  • Solidity の function = Rust の fn. function add(uint a, uint b) public returns (uint)fn add(&self, a: U256, b: U256) -> U256、Solidity の view / pure 修飾は Rust の &self / 引数のみで暗黙。
  • Solidity の msg.sender = Rust では明示引数. Solidity は EVM が暗黙提供、Rust では関数引数で受け取る or context 構造体経由、暗黙でない。
  • Solidity の event = Rust では Event 構造体 + emit. Alloy では sol! マクロで Solidity ABI を Rust 型に変換、type-safe な emit。
  • Solidity の require / revert = Rust の Result<T, E>. require(cond, "msg")if !cond { return Err(...) }? 演算子で伝播。
  • Solidity の mapping = Rust の HashMap. mapping(address => uint)HashMap<Address, U256>、Rust ではキー / 値の型を明示。
  • Solidity の inheritance = Rust の trait composition. contract A is B, Cimpl Trait1 + Trait2 for Struct、Rust は trait による mixin。
  • Rust の async = Solidity にない. Solidity は同期的、Rust は async/await + Future、Alloy / Reth の RPC は全部 async。

具体例 + ステップで組み立てる

Solidity エンジニアのための Rust — 移行マップ

Solidity でコントラクトを書いてきたなら、EVM 挙動で気にすべき点はすでに身についている。足りないのは、そのコントラクトを走らせるエンジン側を読むための Rust の見方 だけである。本レッスンは、続く Rust 章に入る前の対訳表としてその差分を埋める。

Rust をゼロから教える回ではない。ここで示すのは、信頼している Solidity の概念が Rust でどう写るか(あるいは写らないか)である。1 時間後に bluealloy/revm を開いたとき、画面いっぱいの Rust を「Solidity で知っていることの別表現」として読める状態を狙う。

📌 対象。 Solidity コントラクトを書いた経験がある読者向けである。未経験ならこのレッスンは飛ばす。続く Rust 章はジェネリクスから直接始まる。

1. 基本データ型はほぼ 1:1

SolidityRust (alloy / revm)備考
addressAddress (= B160 = 20 byte 固定配列)同じ 20 byte。型付きラッパ
uint256U256256-bit 符号なし。alloy-primitives で定義、Rust EVM ツール全部が使う
int256I256同じだが符号あり
bytes32B256 (= 32 byte 固定配列)ハッシュ・スロットキー・tx ハッシュに使う
bytesBytes (Vec<u8> のラッパ)動的バイト列
stringString同じ概念; UTF-8 所有文字列
boolbool同じ
mapping(K => V)HashMap<K, V>ただし — §3 の所有権を参照
uint256[]Vec<U256>ヒープ確保の伸長可能ベクタ

型はほぼ同型 である。U256::from(100)Address::from_slice(...)B256::random() は Solidity の uint256(100)address(0x...) と同じ感覚で扱える。

🔍 リポジトリで確認。 alloy/crates/primitives/src/AddressU256B256 がある。1 ファイル開いて確認する。筋肉記憶になっている Solidity 型は、この 1 つの crate に集約されている。

2. Contract の形 ≈ struct + impl

Solidity の contract は 状態 (storage フィールド) + 挙動 (関数)。Rust はこれを 2 つの宣言に分ける:

contract Vault {
    mapping(address => uint256) public balances;
    address public owner;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
}

Rust 版(概念上 — 実際には contract をこう書かない; これは の話):

struct Vault {
    balances: HashMap<Address, U256>,
    owner: Address,
}

impl Vault {
    fn deposit(&mut self, sender: Address, value: U256) {
        *self.balances.entry(sender).or_insert(U256::ZERO) += value;
    }
}

変わった点:

  • msg.sendermsg.value が消える。 どちらも明示パラメータになる。Rust は呼び出し側への受け渡しを強制する(Revm も同様)。
  • payable がない。 Solidity/ABI 側の約束であり、Rust の関数概念には直接存在しない。
  • &mut self が新しい。 deposit が状態を書き換える関数であることを型で宣言する。

&mut self は、deposit 実行中に他の誰もこの struct を読み書きしていない ことを型で保証する。Solidity では EVM 上は同時実行しないが、言語としてその保証を表現しにくい。Rust はこの保証を型システムで強制し、同時書き換えバグを設計段階で排除する。

3. 所有権: Solidity に対応物がない部分

ここからは Solidity の直感が効きにくい。Solidity に 所有権 の概念はなく、値は storage(永続)か memory(呼び出し単位)に置かれる。「所有者は誰か」を意識しないのは、答えが常にコントラクトだからである。

Rust はすべての値で、この質問への回答を要求する。

Solidity 側Rust 側なぜ重要か
暗黙の storage&self / &mut self / 所有する self「読むだけか・書き換えるか・消費するか」が関数のシグネチャに現れる
memory キーワードデフォルト挙動 — Rust の値は box しない限りスタック上キーワード不要
暗黙の値コピー所有型は明示的な .clone()、プリミティブは安価な Copy「この代入は高くつくか?」が呼び出し地点で見える
参照安全性なしライフタイム ('a) が参照の有効期間を記述参照が借用元より長生きしうる場合、コンパイラがそれを拒否する

捉え方の反転は、Rust ではどの値も、ある時点で所有者がただ 1 つだけ という点にある。所有者だけが借用を配れ、&mut の借用は 1 つだけ、または & の借用を任意数という制約になる。これが GC なしで並行コードを安全に保つ仕組みであり、最初の 2〜3 週間で文法を重く感じる理由でもある。

良いニュースとして、これから読む EVM ソースの大半は所有権の「退屈な」領域に留まります — struct が状態を持ち、関数が &mut& を取る、それで 9 割。関数の境界を抜けるライフタイムや Pin、自己参照 struct のような難物は非同期 / unsafe の隅でしか出てこず、opcode 本体にはほぼ現れません。

ちなみに Solidity には、「他の誰かが balances を書き換えていないか」を問う手段がない。並行性がないためである。Rust は &mut を書くたびにこの問いを課す。Reth / revm で答えが「誰も書き換えない」から「誰かが書き換えうる」に変わるのは、トレース、複数 ExEx サブスクライバ、複数スレッド fuzz などを並行実行する場面である。エンジン側ではこの判断が必須で、所有権がその表現手段になる。

4. エラー: requireResult になる

Solidity:

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

Rust:

fn withdraw(&mut self, sender: Address, amount: U256) -> Result<(), VaultError> {
    let balance = self.balances.get(&sender).copied().unwrap_or(U256::ZERO);
    if balance < amount {
        return Err(VaultError::InsufficientBalance);
    }
    *self.balances.get_mut(&sender).unwrap() -= amount;
    Ok(())
}

押さえるべき違いは 2 つである。

  • Result<T, E> は enum である。関数は OkErr を返し、呼び出し側は処理か ? 伝播を選ぶ。コンパイラはエラー無視を許さない。
  • エラーは型付き である。VaultError の各バリアントが失敗理由を明示し、Revm の InstructionResult や Reth の StageError と同じ設計思想で読める。

require(...) は失敗パターンの 1 つ(文字列付き revert)にすぎない。Result は失敗の一般形である。これが腹落ちすれば、「ここで X が失敗したらどうなるか」は戻り型を読むだけで答えられる。

5. 継承が消える — trait が仕事をする

Solidity の is (継承):

contract ERC20Token is Ownable, ReentrancyGuard {
    function transfer(address to, uint256 amount) public onlyOwner nonReentrant { ... }
}

Rust にクラス継承はない。代わりに trait がある — Java/C# のインターフェースに似ているが、重要な違いがある:

trait Token {
    fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Error>;
}

trait Ownable {
    fn owner(&self) -> Address;
    fn assert_owner(&self, caller: Address) -> Result<(), Error> {
        if self.owner() != caller {
            return Err(Error::Unauthorized);
        }
        Ok(())
    }
}

impl Token for MyToken { /* ... */ }
impl Ownable for MyToken { /* ... */ }

Solidity エンジニアにとって実用上重要なのは 2 点である。

  • デフォルト実装を書ける。 上の assert_owner のように trait 側が実装を提供できる。実装側は上書きしない限り自動で受け取る。alloy の Providerroot() 1 つから多数の RPC デフォルト実装を導出するのも同じ仕組みである。
  • ダイヤモンド継承問題がない こと。struct は複数の trait を実装でき、継承の順序という概念がありません。Solidity で Ownable + ReentrancyGuard + Pausable を「継承」すると微妙な落とし穴に出会いますが、Rust なら impl Trait1 + impl Trait2 + impl Trait3 と並べるだけ。順序にも依存しません。

Reth のソースは トレイト密度が高いStageProviderDatabaseNetworkSigner など、各トレイトは実装契約を宣言する。Solidity の interface IERC20 と同じ感覚で読めるようになると、コードベース全体が見通せる。

6. 怖い 2 つの語: ライフタイムと async

ここからは Solidity に直接対応しない領域である。ここでは事前オリエンテーションとして 2 段落だけ示し、詳細は後続レッスンに譲る。

ライフタイム ('a) は参照の有効期間を示す。Solidity には関数境界を越える参照概念がないが、Rust では参照が生き残るため、コンパイラが「借用元より長生きしない」ことを検証する。fn foo<'a>(x: &'a Bar) は参照 x の生存期間を示す。大半は推論されるが、明示が必要な残りの場面がバグ混入点になる。

async fn.await は Rust の non-blocking I/O 表現である。Solidity が 1 tx 内で同期実行されるのに対し、Rust ノードは RPC・P2P・ディスクを同時進行する。async は「待っている間に他の仕事を進める」記法であり、表面上は async fn.await の 2 つを押さえればよい。

7. Solidity → Rust 移行チートシート

1 時間後にソースを読むときに:

Rust で見えるものSolidity に対応するもの持ち越し方
U256AddressB256uint256addressbytes32同じ byte、型付きラッパ
struct Foo { ... }contract の状態フィールド状態の形
impl Foo { ... }contract の関数挙動
&mut self暗黙の storage を mutate する関数コンパイラ検証済み排他
Result<T, E>require / revert型付きエラー。? 演算子で伝搬
trait X { ... }interface IX + おそらく librarytrait はデフォルト impl を持てる
Option<T>「ゼロかも / 見つからないかも」Some(value)None
Arc<T>「共有・複数 reader」共有所有レッスンで扱う
Box<dyn Trait>実行時多態ヒープ確保 + vtable
async fn / .await(対応物なし)Non-blocking I/O。別レッスンで扱う
ライフタイム ('a)(対応物なし)コンパイラ強制の参照スコープ

続く 4 レッスン(ジェネリクス・共有所有・unsafe・マクロ)は、ソース読解 tier の背骨となる Rust を扱う。本レッスンを先に通すと、各回の着地が安定する。

ドリル

  1. Solidity コントラクトを 1 つ翻訳してみる。 自分で書いた小さなコントラクト(あるいは solady の ERC20)を選んで、紙の上で同じ状態と 1〜2 個のメソッドを持つ Rust の struct + impl を書いてみてください。動かさなくていい — 形だけで構いません。30 分。
  2. alloy の Rust struct を 1 つ読む。 alloy/crates/primitives/src/address.rs を開き、Address 型を探す。何が struct、何が impl ブロック、何が trait の impl かを観察する。Solidity に対応物がない部分(ライフタイム、derive、属性マクロ)を見つける。30 分。
  3. エラー処理を比較する。 require を 2 つ持つ Solidity 関数を選び、2 バリアントのカスタムエラー enum と Result を使う Rust へ書き換える。型が 失敗モードをどう文書化するかを観察する。45 分。

このレッスンを終えると、続く Rust 章(ジェネリクス・Arc・unsafe・マクロ)を、既存モデルに追加される語彙として読める。ゼロから Rust を学ぶのではなく、エンジン層コードへ Solidity の直感を翻訳する Rust 慣用句を学んでいる と捉える。

まとめ(3行)

  • 型システム 1:1(U256 / Address / B256)、状態変数 → struct field、関数 → fn、msg.sender → 明示引数、require → Result<T, E>。
  • Mapping → HashMap、inheritance → trait composition、event → sol! マクロ + emit、Rust は all async(Solidity にない)。
  • 所有権 + 借用は新概念(最初の壁)だが、Solidity 経験で型システム / 制御フロー / ガス意識は持ち越せる、次は generics と trait bounds。