前章で、形の同じ 20 バイトの識別子が4つ出てきました。
0xa60eF4E6E8F821F3bd5D42f8067BD5d4a96e0CEf ← 登録した「人」
0x04ECc497632847529FC431bA780Ffc573Fe4aC47 ← 作られた NFT
0x6011B8BA5AcD3BBb925Fa793E125BfE750ae3655 ← アクセストークン
0xEF62FB495266C72a5212A11Dce8baa79Ec0ABeB1 ← NFT を作った工場
見た目では区別がつきません。1つ目だけが人間で、残り3つはプログラムです。
本章では、この 20 バイトが何なのかを分解します。あわせて、アーカイブズの識別子として使えるのかという問いに答えます。結論を先に言うと、単体では使えません。理由を見ていきます。
3段階の一方通行
鍵を持つアカウント(EOA、externally owned account)のアドレスは、こう作られます。
秘密鍵 32バイト 誰にも渡さない
↓ 楕円曲線の掛け算(secp256k1) ※戻せない
公開鍵 64バイト
↓ keccak256 ※戻せない
ハッシュ 32バイト
↓ 下位20バイトだけ取る ※戻せない
アドレス 20バイト
矢印はすべて一方通行です。アドレスから公開鍵を求めることも、公開鍵から秘密鍵を求めることもできません。
「下位20バイトだけ取る」に注目してください。32 バイトのうち前半 12 バイトは捨てています。短くするためです。取引のたびに書き込むものなので、公開鍵の 64 バイトをそのまま使う場合と比べて 44 バイト短くできるのは大きい(保管とログの費用の話は 6 章)。
実際にやってみる
付属ツールが使った実データです。この本のアドレスは 59 件の取引を送っているので、そのうち 1 件の署名から公開鍵を復元できます。取引の署名(v, r, s)と署名対象から公開鍵を逆算する ecrecover という計算を使いました。
復元した公開鍵(65バイト、先頭 0x04 は「非圧縮形式」の目印)
0x04 2fe28b23182f82cee954187f4d56c8ecf6a8a735…639b517
keccak256(先頭の 0x04 を除いた 64 バイト)
0xadf2b17c5269e7c782fac164 a60ef4e6e8f821f3bd5d42f8067bd5d4a96e0cef
└─ 捨てられる 12 バイト ─┘ └────── これがアドレス ──────┘
→ 0xa60ef4e6e8f821f3bd5d42f8067bd5d4a96e0cef
一番下の値が、取引の送信者としてチェーンに記録されているアドレスと一致します。
大文字小文字には意味がある
同じアドレスを2通りに書けます。
0xa60ef4e6e8f821f3bd5d42f8067bd5d4a96e0cef ハッシュの生の値
0xa60eF4E6E8F821F3bd5D42f8067BD5d4a96e0CEf EIP-55 を適用したもの
16 進数なので a と A は同じ値です。ですが Ethereum では、この大文字小文字のパターン自体を検査符号として使います(EIP-55)。
EIP / ERC は Ethereum の改善提案に付けられた通し番号で、番号がそのまま規格名として使われます(EIP が提案全般、ERC はそのうちアプリケーション寄りの規格)。以降 ERC-721、ERC-1167 といった番号が出てきますが、いずれもこの通し番号です。
小文字版(0x を除いた 16 進文字列)を keccak256 でハッシュし、i 文字目のハッシュ値が 8 以上なら i 文字目を大文字にする、という仕組みです。0〜9 には大文字小文字がないので、実際に変わるのは a〜f の位置だけです。1文字でも打ち間違えるとパターンが崩れるので、ウォレットが送信前に弾けます。
見た目の装飾ではなく、打ち間違いで資産を失わないための仕掛けです。
そして実務上の注意として、大文字小文字を変えると値が変わる処理があります。たとえばこの本で扱った資産の識別子(DID、Decentralized Identifier)は
did:op: + sha256( 大文字小文字を付けたアドレス + チェーンID )
で計算されるため、全部小文字にすると別の識別子になってしまいます。「同じアドレスだから同じはず」と思って躓く箇所です。
契約のアドレスには鍵が無い
ここが本章の要点です。
冒頭の4つのうち、公開鍵から作られたのは1つ目だけです。残り3つはプログラム(契約)で、その 20 バイトはまったく別の方法で決まります。
| 鍵を持つアカウント(EOA) | 契約 | |
|---|---|---|
| 20バイトの材料 | 公開鍵のハッシュ | 「作った人のアドレス + そのときの連番」のハッシュ |
| 秘密鍵 | ある | 無い |
| 公開鍵 | ある | 無い |
| 自分から取引を送れるか | 送れる | 送れない(呼ばれて動くだけ) |
| 誰が動かすか | 鍵を持っている人 | コードに書いてあるとおり |
契約には秘密鍵が存在しません。 「契約の持ち主」という概念はコードの中の話であって、鍵で守られているわけではありません。
見分ける方法
アドレスを眺めても分かりません。チェーンに問い合わせるしかありません。「そのアドレスにコードが置かれているか」を尋ねます。
尋ね方 1: 付属ツールで見る
いちばん簡単なのは、付属ツールで取引を開くことです。出てくるアドレスそれぞれに「鍵を持つアカウント(人)」か「契約(プログラム)」かの札が付き、コードのバイト数も表示されます。
ツールは裏で次の問い合わせをしているだけなので、自分でやることもできます。
尋ね方 2: 自分で問い合わせる
チェーンには誰でも問い合わせられる窓口(RPC)が公開されています。ウォレットもアカウント登録も要りません。ターミナルから 1 行です。
curl -s -X POST https://ethereum-sepolia-rpc.publicnode.com \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xa60eF4E6E8F821F3bd5D42f8067BD5d4a96e0CEf","latest"]}'
eth_getCode は「このアドレスに置かれているコードを見せてください」という問い合わせです。鍵を持つアカウントの場合、返ってくるのは空です。
{"jsonrpc":"2.0","id":1,"result":"0x"}
0x は「中身なし」を意味します。コードが 0 バイト、つまり人(の鍵)です。
同じ問い合わせを NFT のアドレスに向けると、こうなります。
{"jsonrpc":"2.0","id":1,"result":"0x363d3d373d3d3d363d739c9ee07b8ce907d2f9244f8317c1ed29a3193bae5af43d82803e903d91602b57…"}
長い 16 進が返ってきました。これが契約のコードです。0x を除いた文字数を 2 で割るとバイト数になります。この例では 45 バイトでした。
ちなみに、この 45 バイトの中に 9c9ee07b8ce907d2f9244f8317c1ed29a3193bae という 40 桁が埋まっているのが見えます。これは転送先——本体を持っているテンプレートのアドレスです。この先の「45 バイトしかないのはなぜか」で分解します。
尋ね方 3: エクスプローラで見る
Sepolia の Etherscan でアドレスを検索する方法もあります。契約であれば「Contract」というタブが現れ、鍵を持つアカウントであれば現れません。
前章の4つを調べた結果です。
| アドレス | コードの大きさ | 種別 |
|---|---|---|
0xa60eF4E6… | 0 バイト | 鍵を持つアカウント |
0x04ECc497…(NFT) | 45 バイト | 契約 |
0x6011B8BA…(アクセストークン) | 45 バイト | 契約 |
0xEF62FB49…(工場) | 16,900 バイト | 契約 |
0 バイトなら人(の鍵)、それ以外はプログラムです。
45 バイトしかないのはなぜか
NFT もトークンも、コードがたった 45 バイトです。実際の NFT の機能は 51 個の関数を持っているのに、明らかに足りません。中身を見てみます。
0x363d3d373d3d3d363d73 9c9ee07b8ce907d2f9244f8317c1ed29a3193bae 5af43d82803e903d91602b57fd5bf3
└─ 前半 10 バイト ─┘ └────── 中央 20 バイト ──────┘ └──── 後半 15 バイト ────┘
3 つに分かれています。
- 前半 10 バイト — 呼び出されたときの引数を写し取り、転送の準備をする。最後の 1 バイトは「続く 20 バイトをアドレスとして読め」という命令
- 中央 20 バイト —
9c9ee07b…3193bae。アドレスがそのまま埋まっています - 後半 15 バイト — 転送先を呼び、返ってきた結果をそのまま呼び出し元へ返す。この呼び方は
delegatecallと呼ばれ、テンプレートのコードを実行しながら、保管領域は呼ばれた側(転送役)のものを使い続けるのが特徴です
処理の中身は 1 行も入っていません。「引数を写して、あちらへ渡して、返ってきたものをそのまま返す」だけです。この形は ERC-1167 として規格化されていて、最小プロキシと呼ばれます。
アクセストークンのほうも見ると、同じ形で埋め込まれたアドレスだけが違いました。
NFT 0x363d3d373d3d3d363d73 9c9ee07b…3193bae 5af43d82803e903d91602b57fd5bf3
トークン 0x363d3d373d3d3d363d73 defd0018…815f038 5af43d82803e903d91602b57fd5bf3
前後は完全に同じで、中央の 20 バイトだけが差し替わっています。転送先を書き換えるだけで、別の種類の契約が作れるわけです。
実体はどこにあるのか
埋め込まれていたアドレスを、同じ eth_getCode で調べます。
| アドレス | 大きさ | 正体 |
|---|---|---|
0x04ECc497… | 45 バイト | 作られた NFT(転送役) |
0x9C9eE07b… | 21,504 バイト | NFT の実体(テンプレート) |
0x6011B8BA… | 45 バイト | 作られたトークン(転送役) |
0xDEfD0018… | 24,017 バイト | アクセストークンの実体(テンプレート) |
0xEF62FB49… | 16,900 バイト | 工場 |
ここで注意が要ります。転送先は工場ではありません。 工場(16,900 バイト)は「契約を作る係」で、実体を持つテンプレート(21,504 バイト)は別の契約です。役割が違います。
- 工場 — 頼まれたら 45 バイトの転送役を新しく置く
- テンプレート — NFT としての処理を実際に持っている。置き換わらず、みんなで共用する
- 転送役 — 資産ごとに 1 つ作られる。中身は持たず、テンプレートを呼ぶ
なぜこの形なのか
費用です。契約を新しく置く操作は、チェーン上で高い部類に入ります。もし資産を作るたびに 21,504 バイトを複製していたら、1 件登録するだけで大きなガスがかかります。
45 バイトの転送役を置いて中身を借りる形にすることで、前章で見た「契約 2 つを新設して約 1,300,000 ガス」に収まっています。
なお、この仕組みには帰結もあります。テンプレートは共用されているので、そこに問題が見つかれば、それを使っているすべての資産に影響します。 逆に、転送役はテンプレートを指しているだけなので、資産そのものは軽いままです。
公開鍵は「載っていない」が、隠れてもいない
チェーンに記録されているのはアドレス(ハッシュ)だけで、公開鍵は書かれていません。
ところが先ほど、私は公開鍵を復元しました。使ったのは署名です。
署名とは、秘密鍵を使って「この内容を、この鍵の持ち主が承認した」という短い値を作る操作です。受け取った側は秘密鍵を知らないまま、その値が本物かどうかを検証できます。チェーンに取引が受け付けられるのは、この検証を通ったときだけです。
Ethereum の署名方式(ECDSA)には、署名と署名された内容から、署名者の公開鍵を逆算できるという性質があります。そのため:
| そのアドレスの状態 | 公開鍵は |
|---|---|
| 一度も取引を送っていない | 誰にも分からない |
| 一度でも取引を送った | 誰でも復元できる |
実害のある話ではありません。公開鍵から秘密鍵は求められないからです。
ただし「将来、量子計算機が楕円曲線暗号を破ったとき、送信履歴のあるアドレスから先に危険になる」という議論の根拠がここにあります。長期保存を扱う立場としては、頭の隅に置いておく価値があります。
アーカイブズの識別子として使えるか
ここまでを踏まえて、実務の問いに答えます。「機関の識別子として、あるいは資料の来歴の担保として、アドレスは使えるか」。
単体では使えません。4つの理由があります。
1. 登録も審査も無い
アドレスは誰でも、いくらでも、無料で、一瞬で作れます。鍵をランダムに 1 つ選べば、それだけでアドレスが決まります。申請先も、承認者も、重複チェックもありません。
本書で読んでいるアドレスも、私がこのデモのために作っただけのものです。「これは国立国会図書館のアドレスです」と主張することは誰にでもできます。チェーンはそれを検証しません。
2. 名乗りと実体を結ぶ仕組みが無い
チェーンが保証するのは「この鍵の持ち主が署名した」までです。その鍵の持ち主が誰なのかは、チェーンの外の話です。
ここを埋めるために、データスペースでは別の仕組み(証明書ベースの身元確認)を併用します。10章で扱います。
3. 一度結びつくと、遡って全部が紐づく
匿名だと言われますが、正確には仮名です。アドレスと実名が結びつく機会——取引所への入金、ブログでの言及、本書のような公開——が一度でもあれば、そのアドレスの全履歴が遡って紐づきます。
本書のアドレスは、まさに今その状態です。59 件の取引すべてが、私の名前と結びついた状態で公開されています。失敗した取引も含めてです。
4. 消せない
前章で見たとおりです。「あとで消す」が効きません。
これは「使えないから駄目」という話ではありません。何を保証する道具なのかを取り違えなければよい、ということです。
アドレスが保証するのは「同一の鍵が署名し続けている」という一貫性です。それは強力で、改竄できません。ですが「その鍵が誰のものか」「書かれた内容が正しいか」は保証しません。
機関の識別子として使うなら、別の層で身元を保証したうえで、そこにアドレスを紐づける必要があります。データスペースがまさにその二階建てになっている理由です。
章のまとめ
- アドレスは 20 バイトの識別子。鍵を持つアカウントなら公開鍵のハッシュ
- 契約のアドレスは作られ方が違い、鍵が存在しない
- 見た目では区別できない。コードの有無をチェーンに聞く
- 大文字小文字は打ち間違い検出の符号。変えると DID などの計算結果が変わる
- 公開鍵は載っていないが、取引を1度でも送れば署名から復元できる
- 識別子としては、登録も審査も名乗りの検証も無い。単体では機関の身元にならない
次章では、この記録を誰がどうやって書き、なぜ書き換えられないのかを見ます。