前章で、形の同じ 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 進数なので aA は同じ値です。ですが Ethereum では、この大文字小文字のパターン自体を検査符号として使います(EIP-55)。

EIP / ERC は Ethereum の改善提案に付けられた通し番号で、番号がそのまま規格名として使われます(EIP が提案全般、ERC はそのうちアプリケーション寄りの規格)。以降 ERC-721、ERC-1167 といった番号が出てきますが、いずれもこの通し番号です。

小文字版(0x を除いた 16 進文字列)を keccak256 でハッシュし、i 文字目のハッシュ値が 8 以上なら i 文字目を大文字にする、という仕組みです。09 には大文字小文字がないので、実際に変わるのは af の位置だけです。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 バイト工場

45 バイトの中身と転送先を示した図。上段は 45 バイトを 3 つに区切っており、前半 10 バイトが「呼び出しをそのまま写す」、中央 20 バイトが「9c9ee07b…3193bae 転送先のアドレス」、後半 15 バイトが「結果を呼び出し元へ返す」と示され、その下に「合計 45 バイト。処理の中身は 1 行も入っていない」と記されている。下段は 3 つの契約の関係で、左の「作られた NFT 0x04ECc497… 45 バイト」から「転送 (delegatecall)」というラベルの矢印が中央の「テンプレート 0x9C9eE07b… 21,504 バイト」へ伸びている。右の「工場 0xEF62FB49… 16,900 バイト」からは「複製を置く」というラベルの破線の矢印が左向きに伸びている。図の下部に、45 バイトは本体はあちらですと伝えるだけの転送役であり実体はテンプレートにあること、テンプレートは 1 つを共用するので資産を作るたびに 21,504 バイトを置かずに済むこと、工場は作る係であって実体を持つテンプレートとは別の契約であることが記されている。

ここで注意が要ります。転送先は工場ではありません。 工場(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度でも送れば署名から復元できる
  • 識別子としては、登録も審査も名乗りの検証も無い。単体では機関の身元にならない

次章では、この記録を誰がどうやって書き、なぜ書き換えられないのかを見ます。