ローカルで動くようになったら、次は「どこに出すか」です。ここで用語の交通整理をしておきます。L1 / L2 / chain ID / RPC URL の 4 つは、混ざりやすいわりに全部違うものです。
L1 とは
それ自体で完結しているブロックチェーンです。自前のバリデータ集合を持ち、自前で合意形成します。Ethereum のメインネットがこれで、Sepolia は Ethereum の L1 テストネットです。本番と同じ仕組みで動く、練習用の別チェーンだと思ってください。
L2 とは
実行は自分でやるが、安全性を L1 に預けているチェーンです。Base(Coinbase が運営する OP Stack ベースの Optimistic Rollup)がその代表です。動きはこうなっています。
- Base が自分のところでトランザクションを順番に処理する(シーケンサ)
- その取引データをまとめて Ethereum L1 に書き戻す(EIP-4844 の blob)
- 状態のハッシュも L1 上のコントラクトに提出する。不正があれば L1 側で異議申し立てできる
つまり取引データは L1 に投稿されているので、Base のシーケンサが停止しても、そこから状態を再構築できます。
ただし blob は永久には残りません。 Ethereum のコンセンサス層は blob を約 18 日で捨てます。それ以降の保存は、その間にデータを取り込んだアーカイブノードやインデクサに依存します。L1 に永続するのは「データそのもの」ではなく「データが確かに投稿されたことの証明」(KZG コミットメント)です。
実務で効いてくる差はここです。
| Sepolia (L1) | Base Sepolia (L2) | |
|---|---|---|
| Chain ID | 11155111 | 84532 |
| ブロック生成 | 約 12 秒 | 約 2 秒 |
| ガス代 | 高め | 桁違いに安い |
| 手数料の内訳 | 実行費のみ | 実行費 + L1 へのデータ書き込み費 |
| 安全性の源 | 自前のバリデータ | Sepolia(L1) |
L2 の手数料が 2 段構えになっているのが特徴です。ただし EIP-4844 で blob が使えるようになってから、L1 データ費は総額の数 % 以下まで下がりました(実測では Base メインネットで 0.2% 程度)。「L2 では calldata を削れ」という定石は、もう当てはまりません。
Base Sepolia と Sepolia は同じ EVM か
EVM としては同じです。 Solidity のコードも、コンパイル済みバイトコードも、そのまま両方で動きます。本書の TicTacToe.sol は 1 文字も変えずに両方へデプロイできます。Foundry も wagmi も、変わるのは RPC の URL と chain ID だけです。
ネットワークとしては完全な別チェーンです。 ここが混同しやすいところで、
- Sepolia の faucet(テストネットの ETH を無償で配る蛇口)でもらった ETH は、Base Sepolia では使えません。ブリッジ(片方のチェーンで預けて、もう片方で受け取る仕組み)で移す必要があります
- 同じアドレスでも残高は別勘定です
- デプロイしたコントラクトのアドレスも別。両方に出すなら 2 回デプロイします
名前が「Base Sepolia」なのは、Base Sepolia が決済先(settlement layer)として Sepolia を使っているからです。本番の Base が Ethereum メインネットに書き戻すのと同じ関係を、テストネット同士で再現しています。
chain ID と RPC URL は別物
これが一番よく混ざります。chain ID がネットワークの「正体」、RPC URL はそこへの「入口」だと考えてください。
実際に確かめるのが早いです。
cast chain-id --rpc-url https://sepolia.base.org
# → 84532
cast chain-id --rpc-url https://base-sepolia-rpc.publicnode.com
# → 84532
別々のサーバなのに、同じ chain ID を返します。 どちらも同じ Base Sepolia を見ている、ただの別の窓口だからです。
| chain ID | RPC URL | |
|---|---|---|
| 正体 | ネットワークそのものの識別子 | そのネットワークへ問い合わせるサーバのアドレス |
| Base Sepolia | 84532(不変) | 何十個も存在する。自前ノードを立ててもいい |
| 誰が決めるか | プロトコルで固定 | あなたが選ぶ(Alchemy / Infura / 公開エンドポイント / 自前) |
| Web 開発で言うと | 本番 DB か staging DB か | どの接続文字列・どのリードレプリカを使うか |
chain ID がプロトコルに埋め込まれている理由
単なるラベルではありません。EIP-155 により、署名するトランザクションの中に chain ID が含まれます。
これがないとどうなるか。あなたが Sepolia 向けに署名した「1 ETH を送る」トランザクションを誰かが拾い、そのままメインネットに流し込めてしまいます。署名は有効なので通ってしまう。これがリプレイ攻撃です。
chain ID を署名対象に含めることで、あるチェーン向けの署名は他のチェーンでは無効になります。ネットワークの取り違えが致命傷にならないための、プロトコルレベルの安全装置です。
実務上の帰結
1. コントラクトのアドレスは chain ID ごとに別勘定です。
だから設定ファイルはこういう形になります。
NEXT_PUBLIC_TICTACTOE_ADDRESS_31337=0x5FbD... # ローカル anvil
NEXT_PUBLIC_TICTACTOE_ADDRESS_84532= # Base Sepolia
NEXT_PUBLIC_TICTACTOE_ADDRESS_11155111= # Sepolia
2. RPC URL は差し替え自由です。
公開エンドポイントがレート制限に当たったら、Alchemy の URL に替えるだけ。コードもコントラクトのアドレスも変わりません。wagmi の設定でも役割が分かれています。
// src/lib/wagmi.ts
const ENDPOINTS: Record<number, string | undefined> = {
[anvil.id]: 'http://127.0.0.1:8545', // ← 入口
[baseSepolia.id]: process.env.NEXT_PUBLIC_RPC_URL_84532 || undefined,
[sepolia.id]:
process.env.NEXT_PUBLIC_RPC_URL_11155111 || 'https://ethereum-sepolia-rpc.publicnode.com',
};
export const config = createConfig({
chains: SUPPORTED_CHAINS, // ← 正体
connectors: [injected()],
ssr: true, // サーバで描いてからクライアントで復元する
transports: Object.fromEntries(
SUPPORTED_CHAINS.map((chain) => [chain.id, http(ENDPOINTS[chain.id])])
),
});
chains を直書きせず SUPPORTED_CHAINS から組んでいる理由は、次の項目です。
3. 提供するチェーンは、アドレスが設定されているものだけに絞ります。
これは実際に踏んだ失敗です。chains に anvil を直書きしたまま Vercel にデプロイしました。 公開ページを開いた Chrome が、「このサイトがローカルネットワーク上の機器へアクセスしようとしています」という許可ダイアログを出しました。
仕組みはこうです。ウォレットが未接続のとき、wagmi は chains[0] を相手に読み取りを始めます。 一覧の先頭が anvil だったので、閲覧者のブラウザがその人自身の 127.0.0.1:8545 に JSON-RPC を投げにいきました。当然そこには何もありませんが、Chrome にはローカルネットワークへの接続に見えます。
ここから帰結が 2 つ出ます。チェーンの順序には意味がある(先頭が未接続の訪問者の相手をする)。そして、手で書いた一覧は本番用に消し忘れる。どちらも、一覧を計算で作れば消えます。
// src/lib/chains.ts
const CANDIDATE_CHAINS = [sepolia, baseSepolia, anvil] as const; // 先頭が既定
export const SUPPORTED_CHAINS = CANDIDATE_CHAINS.filter(
(chain) => validate(CONFIGURED_ADDRESSES[chain.id]) !== undefined
);
ローカルでは NEXT_PUBLIC_TICTACTOE_ADDRESS_31337 が .env.local にあるので anvil が残り、Vercel にはその変数を置かないので anvil が消えます。「本番の設定を忘れない」ではなく、「設定がなければ自動で消える」に変えるのが要点です。デプロイ先が知らないチェーンは、そもそも選択肢に出せません。
4. メインネットを一覧から外しておくと安全です。
本書のアプリは候補(CANDIDATE_CHAINS)にメインネットを入れていません。ウォレットが誤ってメインネットに繋がっていても、アプリ側が「非対応のネットワーク」として弾きます。ブラウザから普通に遊んでいる限り本物の資金は賭けられないようにしてあります。学習用のコードでは、これは書いておく価値のある 1 行です。
どのテストネットを使うか
学習用としては Base Sepolia を主軸にするのが実用的です。faucet が枯れにくく、確認が速く、ガスがほぼ無料。
ただ「L1 に直接デプロイする感触」も一度は踏んでおくと理解が進むので、本書では両方を設定に残しています。ローカルの anvil を含めて 3 つです。
| 用途 | チェーン | Chain ID |
|---|---|---|
| ローカル開発 | Anvil | 31337 |
| 公開テスト(推奨) | Base Sepolia | 84532 |
| 公開テスト(L1 の定番) | Sepolia | 11155111 |
次章では、この anvil に MetaMask を繋ぎます。ここにも独特のつまずきポイントがあります。