レッスン4 — Solidity エンジニアのための Rust — 移行マップ
問い
Solidity を書いてきた人が Rust source(Reth / Revm / Alloy)を読むときの 移行マップ。Solidity の概念と Rust の対応 + 違い + 注意点。Solidity 経験が活きる場所と頭の切り替えが必要な場所を整理。
原理(最小モデル)
- 型システム. Solidity の
uint256→ Rust のU256(256 ビット)/address→Address(20 バイト)/bytes32→B256、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, C→impl 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
| Solidity | Rust (alloy / revm) | 備考 |
|---|---|---|
address | Address (= B160 = 20 byte 固定配列) | 同じ 20 byte。型付きラッパ |
uint256 | U256 | 256-bit 符号なし。alloy-primitives で定義、Rust EVM ツール全部が使う |
int256 | I256 | 同じだが符号あり |
bytes32 | B256 (= 32 byte 固定配列) | ハッシュ・スロットキー・tx ハッシュに使う |
bytes | Bytes (Vec<u8> のラッパ) | 動的バイト列 |
string | String | 同じ概念; UTF-8 所有文字列 |
bool | bool | 同じ |
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/にAddress、U256、B256がある。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.senderとmsg.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. エラー: require は Result になる
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 である。関数はOkかErrを返し、呼び出し側は処理か?伝播を選ぶ。コンパイラはエラー無視を許さない。- エラーは型付き である。
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 のProviderがroot()1 つから多数の RPC デフォルト実装を導出するのも同じ仕組みである。 - ダイヤモンド継承問題がない こと。struct は複数の trait を実装でき、継承の順序という概念がありません。Solidity で
Ownable + ReentrancyGuard + Pausableを「継承」すると微妙な落とし穴に出会いますが、Rust ならimpl Trait1+impl Trait2+impl Trait3と並べるだけ。順序にも依存しません。
Reth のソースは トレイト密度が高い。Stage、Provider、Database、Network、Signer など、各トレイトは実装契約を宣言する。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 に対応するもの | 持ち越し方 |
|---|---|---|
U256、Address、B256 | uint256、address、bytes32 | 同じ byte、型付きラッパ |
struct Foo { ... } | contract の状態フィールド | 状態の形 |
impl Foo { ... } | contract の関数 | 挙動 |
&mut self | 暗黙の storage を mutate する関数 | コンパイラ検証済み排他 |
Result<T, E> | require / revert | 型付きエラー。? 演算子で伝搬 |
trait X { ... } | interface IX + おそらく library | trait はデフォルト 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 を扱う。本レッスンを先に通すと、各回の着地が安定する。
ドリル
- Solidity コントラクトを 1 つ翻訳してみる。 自分で書いた小さなコントラクト(あるいは
soladyの ERC20)を選んで、紙の上で同じ状態と 1〜2 個のメソッドを持つ Rust の struct + impl を書いてみてください。動かさなくていい — 形だけで構いません。30 分。 - alloy の Rust struct を 1 つ読む。
alloy/crates/primitives/src/address.rsを開き、Address型を探す。何がstruct、何がimplブロック、何が trait の impl かを観察する。Solidity に対応物がない部分(ライフタイム、derive、属性マクロ)を見つける。30 分。 - エラー処理を比較する。
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。