T5 · EVM Account Model
EOA とコントラクトアカウントは何が違う?
- 練習する能力
- BuilderProtocol Literacy
- 実践
- RPC でコントラクトの storage slot を直接読み、getter の戻り値と比べる。
- AI Lab
- AI にコードのガス内訳を計算させ、実際の取引レシートとの差を確認する。
現実の問題
三人のうち二人が同意しないと出金できない、チームの金庫を作ります。
設計会議では「マルチシグウォレットのアドレスで受け取る」と「金庫コントラクトを配備する」という二案が出ます。成果物を外から見ると同じです。どちらも 0x で始まる四十桁の十六進文字列で、資金を受け取れ、エクスプローラーで残高を調べられます。
そこで三つ質問しますが、明快な答えがありません。
- このアドレスは自分から取引を開始できますか。
- このアドレスへの送金は失敗し得ますか。
- このアドレスの規則を変更できるのは誰ですか。
さらに根本的な疑問があります。**配備したコードと、そのコードが覚えているデータはどこに保存されていますか。**データベースのように読み出せますか。
同じ見た目の二つのアドレスは、どこが違うのでしょう。
思考実験
チェーン全体を世界に一台だけのコンピューターと考えます。巨大な表を一つだけ持ちます。
key はアドレス、value は四フィールドです。
nonce カウンター
balance 残高
codeHash コードのダイジェスト
storageRoot このアドレス専用の小さな表のダイジェスト違いは codeHash が空かどうかに尽きます。
空なら鍵に対応する位置で、所有者が秘密鍵で署名して取引を開始でき、専用の小さな表は常に空です。
空でなければコードと専用の表を持ちます。自分から何も開始できませんが、呼ばれるとコードが実行され、その表を読み書きできます。
ここから三つを導きます。
**第一に、このコンピューターには時計がありません。**cron、タイマー、バックグラウンドスレッドはありません。取引が届き、実行し、状態を変え、次の取引を待つだけです。二つの取引の間は完全に静止します。
第二に、すべての計算に料金が必要です。 各ノードが同じコードを実行し結果を保存します。 各操作に価格があり、最も高いのは小さな表への書き込みです。すべてのノードが永久保存するからです。
**第三に、支払えるのは第一種のアドレスだけです。**コントラクトには秘密鍵がなく署名できません。実行連鎖は必ず秘密鍵を持つ人の取引から始まります。複雑なコントラクトも、その連鎖に動かされます。
冒頭の二案は同一ではありません。表の第三フィールドが違えば、自分で動けるか、入金時にコードが動くか、規則をどこに書くかが変わります。
あなたが決める
預金者へ毎日一回利息を渡す預金コントラクトを、どう実装しますか。
結果を観察する
共通する構造は、すべての状態変化で、秘密鍵を持つ人が取引に署名し、その変化に支払う必要があることです。
| 案 | 起動する人 | 支払う人 | 追加する依存 |
|---|---|---|---|
| コントラクトのタイマー | 存在しない | — | — |
| 操作時に精算 | 次の利用者 | その利用者 | なし。ただし時刻を管理できない |
| 自社の定期処理 | 自社サービス | 自社 | 常時稼働する集中型サービス |
| 自分で受け取る | ユーザー | ユーザー | なし。ただし認知と支払意思が必要 |
ここから一般的な二つの結論が得られます。
**一、誰が払うかは製品設計の問題です。**費用の置き場所が機能の形を決めます。ユーザーの費用をゼロにすれば、自社が負担し、ホットウォレットと運用が必要になります。
二、コントラクトのデータ構造は Gas によって決まります。従来のバックエンドで一行のループでも、全ユーザー走査はここでは失敗する取引です。累積指数、遅延精算、Merkle リストなど、一見奇妙な設計は同じ制約から生まれます。
モデルを作る
一つの表、四つのフィールド:
アドレス → {
nonce EOA:送信した取引数;コントラクト:作成したコントラクト数
balance wei 単位のネイティブ通貨残高
codeHash 空 → EOA;非空 → コントラクトアカウント
storageRoot このアドレスの storage ツリーのルート
}二種のアカウントの比較:
| EOA | コントラクトアカウント | |
|---|---|---|
| 作り方 | 秘密鍵から導出し、オンチェーン操作は不要 | 配備取引で作成 |
| 自分から取引を開始 | 可能。唯一の起点 | 不可能 |
| コード | なし | あり |
| storage | なし | あり |
| nonce | 送信済み取引の番号 | 作成したコントラクト数 |
| ネイティブ通貨の受取 | 成功する | コードによって失敗し得る |
| 規則の変更者 | 秘密鍵の所有者 | コード次第。変更不可の場合も、一アドレスが決める場合もある |
最後の二行が金庫の問いの答えです。コントラクトへの送金は失敗し得ることを覚えてください。受取ロジックがなければロールバックされますが、手数料は引かれます(T3)。
storage の構造:
各コントラクトは独立したアドレス空間を持ち、0 から始まる各スロットは 32 バイトです。配置規則は決定的です。
| 変数の種類 | スロット位置 |
|---|---|
| 固定長変数 | 宣言順に 0 から配置し、32 バイトに収まるものはまとめる |
| 動的配列 | 宣言位置に長さ、要素はそのスロット番号のハッシュから連続配置 |
| マッピング | 宣言位置は空。key とスロット番号を連結したハッシュ位置に値を置く |
**チェーン上に非公開変数はありません。**Solidity の private は他コントラクトのコードからの読み取りを制限するだけで、チェーン外からスロット番号で読むことは防ぎません。「実例」で大きな損失の例を扱います。
Gas の構造:
取引費用の構成を、高価なものから大まかに並べます。
storage をゼロから非ゼロへ書く 最も高い。全ノードが新たに永久保存する
非ゼロの storage を更新する 高いが、上より明らかに安い
storage を読む 中程度。同じ取引の二度目はかなり安い
memory で計算する 安い。取引終了で破棄する
calldata を読む 最も安い。すでに取引に含まれる
取引の基礎費用 + calldata のバイト料金 コードと無関係の固定部分実装に直結する三つの推論です。
- Gas 節約は storage 書き込みの削減が最優先、次が読み取り、最後が計算です。O(n) を O(log n) にするより、一度の書き込みをループ外へ出すほうが効く場合があります。
- **同じ取引でも、初回のスロットアクセスと後続では価格が大きく違います。**T8 のコールド/ウォームアクセスで、最適化には実測が必要になります。
- **数字はプロトコル更新で変わるので暗記しません。**相対関係と、
eth_estimateGasと実際のレシートで測る習慣を覚えます。
何と呼ぶか
秘密鍵で管理するアカウントです。アドレスは公開鍵のダイジェスト(T2)で、オンチェーン操作なしで存在します。未使用のアドレスにも送金できます。
すべての実行の唯一の起点です。呼び出し連鎖をさかのぼると、EOA の署名にたどり着きます。
配備取引で作成され、コードと storage を持つアカウントです。秘密鍵がなく、自分から取引を開始できません。
呼ばれた時にコードを実行するだけです。配備後は通常変更できず、「更新」はプロキシで別アドレスへ転送します。T8 でその落とし穴を扱います。
EOA では送信済み取引数(T3 で取引を詰まらせたもの)、コントラクトでは作成したコントラクト数という二つの意味があります。
新しいコントラクトのアドレスは作成者のアドレスと nonceから導かれるため、予測できます。
コントラクト専用の表の一マスです。番号も内容も 32 バイトです。
チェーン上で最も高価な資源で、取引後も残るものです。高い理由は、全世界のノードがこの 32 バイトを永久保存するからです。
計算量とストレージ使用量の価格単位です。各操作に Gas 価格があり、支払額は消費 Gas と単価の積です。
本質は料金徴収ではなく、共有コンピューターの資源に値段を付けることです。無限ループで全世界を止めることを防ぎます。T3 で二つの上限パラメータの問題を扱いました。
全アカウントの巨大な表です。ブロックヘッダーには表自体でなく、そのダイジェストである状態ルートを保存します。
T1 の説明どおり、状態はチェーンに「書く」のでなく全取引を順に実行して導きます。チェーンが約束するのは結果のダイジェストです。
実践
**取引は送らず、秘密鍵も実資産も使いません。**テストネットまたはメインネットの読み取りエンドポイントを使えます。読み取りは無料です。
まず二種類を区別します。
curl -s -X POST <自分の-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
"params":["<あるアドレス>","latest"]}'0x なら EOA、長いバイトコードならコントラクトです。唯一信頼できる判別方法で、文字列の見た目では区別できません。
自分のウォレット、トークンコントラクト、適当に作ったアドレスを試します。最後も正常に 0x を返します。未使用アドレスと EOA はチェーン上で区別されません。
コントラクトの nonce の意味を見ます。
curl -s -X POST <自分の-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount",
"params":["<あるコントラクトアドレス>","latest"]}'子コントラクトを作るファクトリーを試すと非ゼロの値が見えます。取引を送れないので送信件数ではなく、作成したコントラクト数です。
slot 0 を読みます。
curl -s -X POST <自分の-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getStorageAt",
"params":["<あるトークンコントラクトアドレス>","0x0","latest"]}'32 バイトが返ります。エクスプローラーでソースを検証済みのトークンを選び、最初の状態変数を見て、その値として解釈します。
文字列や複数の短い変数が詰められていると直感的でない結果になります。それこそ観察対象です。storage は変数単位でなく 32 バイトのスロット単位です。
eth_call で同じデータの getter を読みます。
curl -s -X POST <自分の-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"<同じトークンコントラクトアドレス>",
"data":"<totalSupply の四バイトセレクター>"},"latest"]}'関数セレクターは関数シグネチャ文字列の keccak256 の先頭 4 バイトです。totalSupply() は固定値なので、自分で計算して確認します。この章で初めて T2 のハッシュを本当に使います。
totalSupply のスロットを探します。
検証済みソースで宣言順に状態変数を数えると、大体のスロットが分かります。パッキングには注意します。eth_getStorageAt で読み、前の eth_call と比較します。
**二つは完全一致するはずです。**片方はコントラクトが読み、もう片方は迂回して直接読みます。
不一致なら数え間違いか、プロキシ構成かもしれません。データはプロキシの storage にあるのに実装側を読んでいる場合です。これ自体も良い発見です。
最も価値のある、マッピングの値を読みます。
balanceOf はマッピングです。アドレスの残高の場所は次の規則です。
スロット番号 = keccak256( 32バイトへ左ゼロ埋めしたアドレス ++ 32バイトへ左ゼロ埋めした宣言スロット番号 )32 バイト二つを 64 バイトへ連結し keccak256 を計算します。任意のツールで構いません。Foundry があれば cast に計算用サブコマンドがあるか調べ、なくても手動連結とハッシュで同じです。
その後、次を実行します。
curl -s -X POST <自分の-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getStorageAt",
"params":["<トークンコントラクトアドレス>","<計算したスロット番号>","latest"]}'eth_call の balanceOf と比較します。一致した瞬間、コントラクトストレージを本当に理解できます。
よくある不一致は、アドレスの小文字化忘れ、32 バイトへの左ゼロ埋め忘れ、連結順の逆転(key が先、スロットが後)、継承階層によるスロットの数え違いです。一つずつ調べ、教訓にします。
ブロックタグの力を実感します。
任意の問い合わせの latest を過去の高さへ変えると、その時点の状態を読めます。
エラーならアーカイブノードではありません。T4 の費用の落とし穴を、自分で体験したことになります。
AI Lab
三段階で行います。
第一段階:
この Solidity コードを一回呼ぶ際の Gas を分解してください。
基礎費用、calldata、storage の読み書き、メモリと計算、ログです。
各数値と根拠、合計を示し、どのプロトコル版の価格か明記してください。
第二段階:
同じアドレスからの初回と二回目では、Gas はどれだけ違いますか。
どの項目が違い、なぜですか。
第三段階:
三つの書き換え案を示し、それぞれの節約量と犠牲を説明してください。次に実測します。テストネットへ配備し二回呼び、両方のレシートを取得します。
curl -s -X POST <自分のテストネット-RPC-アドレス> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<ハッシュ>"]}'gasUsed と推定合計を比較します。
**重点は第二段階と差分です。**コールド/ウォームの導入や払い戻し削減など、価格は繰り返し変わりました。モデルは各版の混じった文章から学ぶため、専門的に見えても複数版の数字をつなぎ、合計が合わないことがあります。
実レシートを渡して差の項目を聞くと、二度目は通常より正確になります。記憶から数値を言わせるのでなく、検証できる基準を渡したからです。
正しい使い方は、モデルで仮説を作り、チェーンで検証することです。
AI の回答後、自分で必ず検証すること
- 各操作の Gas 数値が最新のプロトコル更新後か、古い価格か確認する
- ゼロからの書き込みと非ゼロスロットの変更を区別する。価格差は大きい
- コールドとウォームの差を含む。同じ取引で二度目のアクセスは安い
- calldata のゼロバイトと非ゼロバイトを区別する
- 払い戻し上限を誤らず、廃止済み規則を使わない
- opcode、コンパイラ設定、テストフレームワークの用法が公式文書で確認できる
- 総推定と実際の gasUsed を比較し、一割以上なら差の理由を問う
実例
更新可能な構成ではユーザーがプロキシを呼び、実装へ転送します。コードは実装から、storage はプロキシ自身のものを使います。
双方の変数はスロット 0 から始まります。プロキシが実装アドレスをスロット 0 に保存し、実装の最初の変数もそこなら、普通の代入でプロキシの実装先を変えてしまいます。
標準的な解決は、固定文字列のハッシュから得る擬似ランダムなスロットに重要変数を置き、衝突確率を無視できるほど下げることです。主要な実装はこの規格に従います。
storage はコードでなくアドレスの属性だと示す、最も直接的な例です。
乱数シード、パスワードのハッシュ、未公開のリストを private 変数に保存し、外部から読めないと思った例があります。
誰でも eth_getStorageAt 一回で読めます。private はコンパイラ向けの可視性修飾子で、他コントラクトのコードを制限し、チェーン外の読み取りは制限しません。
抽選、予想、封印入札で繰り返し起き、先に結果を知って賭ける人が現れます。チェーンに秘密はありません。秘密にはコミット・リビールなどが必要です。
初期 EVM はスロットの消去に大きな払い戻しを与えました。そこで安い時に無意味な storage を大量に書き、高い時に消して払い戻す、安い Gas を保存するような仕組みが作られました。
実際の取引市場も存在しましたが、プロトコル更新で払い戻しが大幅削減され、その経路はすぐ無効になりました。
**教訓は払い戻し自体でなく、Gas 価格の細部に依存する事業の脆さです。**価格は合意規則の一部で、変わります。
貸付の清算は自動ではありません。条件を満たせば誰でも清算でき、報酬を与えるとコードに書きます。
極端な相場で報酬が手数料に届かず、誰も呼びたがらない例がありました。債務超過でもチェーン上では何も起きず、損失をプロトコルが負担しました。
cron がないことの高価な実演です。誰かが呼ぶ設計では、その人の利益を計算する必要があります。T22 で全体を扱います。
変数を一つ変える
誰もが任意のデータを入れ、状態は無限に膨張します。ディスクとメモリが普通のハードウェアを超え、わずかな機関しか運用できなくなります。
誰でも独立して検証できるチェーンではなくなります。
Gas は料金だけでなく、分散性を維持する門です。保存済み状態への継続課金の議論も、一回の料金で永久保存する収支が合わないため続いています。
取引を始めるアカウントとロジックを持つアカウントの境界が消えます。ウォレットが取引で一時的にコードを持ち、バッチ、手数料代払い、上限、ソーシャルリカバリーを実現できます。
この方向の提案は進み、比較表の数行が変わります。ただし基本モデルは変わりません。同じ表、四フィールド、時計のないコンピューターです。
**だから API より先にモデルを学びます。**API は毎年変わっても、四フィールドは変わっていません。
このモデルではできません。どのアカウントのどのスロットを変えるかは、実行して初めて分かるからです(T1)。スケジューラーは事前に競合を判断できません。
アクセスするアドレスとスロットを事前宣言する任意の仕組みはありますが、Gas 価格にだけ影響し、強制でもスケジューリング用でもありません。
**事前宣言を強制し、それに基づいてグループ化すれば、別のモデルになります。**それが T6 です。
コードが規則だという約束は失われ、ユーザーは次の瞬間の規則を確かめられません。
実際は折衷で、コードは不変でもプロキシから新アドレスへ転送できます。問題は誰がその切り替え権限を持つかです。多くはマルチシグ、時には単独署名です。
**分散性を見るなら、ホワイトペーパーより更新権限を見るほうが有用です。**T23 の中心的な問いです。
持ち帰る問い
状態変化のたびに誰かが払い、その支払者は EOA です。四案は誰に払わせるかを答えます。プロトコル(ホットウォレットと運用)、次のユーザー(高額かも)、受益者(少額で請求に見合わないかも)です。この選択が製品を決めます。実装の細部ではありません。
storage は世界中から読めます。private に機微データを置くリスクは、知らされないユーザーが負います。更新権限を持つ人も全預金者のリスクを握ります。二点とも設計時に明記し、監査まで先送りしません。
Gas は全ネットワークの浪費を高くし、呼ぶ人に利益を要求するインセンティブです。清算、精算、受取が行われないのは境界でこれが崩れるためです。外部呼び出しに依存する機構では、まず呼ぶ人が儲かるか計算します。
章末チェック
eth_getCode の返却が 0x なら EOA、バイトコードならコントラクトです。アドレス文字列自体では区別できません。
落とし穴は、配備取引の実行中は eth_getCode が空を返すことです。これで呼び出し元がコントラクトか判定する防御は、コンストラクターからの呼び出しで回避できます。
決定論的な状態機械で、全ノードの結果を一致させるためです。自動起動は起動時刻そのものを合意の問題にします。
チェーンに cron はありません。自動に見える処理の裏では、ユーザー、自社サービス、報酬で集まる第三者などが取引を送り、支払っています。
読めます。private は他コントラクトの参照を制限するだけで、外部から eth_getStorageAt で直接読めます。
**チェーンに秘密はありません。**秘密には、先にハッシュを出し後で原文を公開するコミット・リビールなどを使います。
二つの理由が重なります。
storage はゼロから非ゼロのほうが非ゼロから非ゼロより高価です。初回は全世界へ永久保存を追加し、二回目は既存値を変更します。
さらにコールド/ウォームアクセスで、同一取引の初回アクセスに追加料金があり、後続は安くなります。
状態によって同じ行の価格が変わるため、Gas 最適化は実測が必要です。
唯一の正解はありません。次を確認します。
- マルチシグウォレットもコントラクトなのでアカウントの種類は同じで、違いはコードの規則です。
- ネイティブ通貨を受け取れるかは受取ロジック次第です。設計時に確認し、当然と考えません。
- 自分で取引を送れないので自動出金はなく、誰かが起動し支払います。
- 規則の変更可否と変更者を文書に明記します。
- 残高と規則は誰でも読める storage にあり、非公開にすべきものを入れません。
- 更新可能なら、更新権限が金庫の最終支配権です。
4 と 6 が明確になって初めて、設計が完成します。
要点
EVM はグローバルな状態機械だ。アカウントは状態の索引であり、ストレージが最もコストの高い部分だ。