はじめに
みなさんはじめまして! 私は弁天、ビットコイン、ブロックチェーン、スマートコントラクトに関する技術と製品を紹介するために生まれた新キャラクターよ。「製品」のほうは公開はもうちょっと先になるけど、第一弾としてビットコインアドレスとトランザクションスクリプトの解説記事よ。
私が持ってるのが何かって? 公開鍵と秘密鍵に決まってるじゃない!
前提知識
この記事を読むには以下の知識があることを前提としているから、不安のある人は先にWebサイトや本で復習しといてね!
-
公開鍵と秘密鍵の役割
-
ハッシュ関数の役割
-
基礎的なC#の知識 ( サンプルソースは NBitcoin を使っているわよ )
公開鍵とアドレス
ビットコインでの支払いは相手の公開鍵に対して行うもので、その公開鍵に対応した秘密鍵を持っている者だけがその中身を取り出せる
おおざっぱな理解としてはこれでいいんだけど、その正体をこれから順番に説明していくわよ。
ビットコイン系列で使う鍵長
まず、ビットコイン、そしてその派生物のビットコインキャッシュやライトコインも、公開鍵・秘密鍵はそれぞれ 256ビットの整数2つ(X座標,Y座標) なのよ。実際には、X座標がわかればちょっと計算することでY座標の候補は2つに絞れるから、情報量としては257ビットね。しかもこの $ 2 ^ {256} $ 個の数値それぞれがみな鍵になれるので、その空間の広大さが総当たり攻撃の困難さを保証してるの。なにしろこれは宇宙に存在する原子の数より大きいんだから。
でもよく使われるビットコインアドレスは
1NC8MrL8UbfV1Sex7ZKh6Lwxomg4G9Q54Y
こんな形になっていて、ぱっと見では257ビットの情報量があるとは思えない長さよね。こんなんで相手の公開鍵が特定できるのかしら? これはなぜなのか説明していきますよ。
公開鍵からバイナリ形式でのアドレス導出
ビットコインアドレスとは、バイナリ形式のアドレスをBase58でエンコードしたものなんだけど、このバイナリ形式のアドレスというのはこういう構造になってるの。
公開鍵そのものではなく鍵のハッシュを使ってデータサイズを削減するのがポイントよ。257ビットが160ビットになったから、異なる公開鍵が偶然同一のアドレスを生成してしまうことは当然ありえるんだけど、160ビットの空間でも充分広いから全然問題にならないわ。
バージョンprefixは種類がいろいろあるけど、有名なものとしては以下よ。P2PKHとP2SHの違いはあとで説明するわ。
|コイン種類|version prefix|
|:-------|--------:|:---------:|
|ビットコイン(P2PKH)|0x00|
|ビットコイン(P2SH)|0x05|
|ライトコイン(P2PKH)|0x30|
|ライトコイン(P2SH)|0x32|
ビットコインキャッシュも、バイナリ形式のアドレスとしてはビットコインと同じよ。(そのあとのBase58のところが、ビットコインキャッシュは独自のものになっている)https://en.bitcoin.it/wiki/List_of_address_prefixes に他の例があるわ。メインネットとテストネットでもversion prefixが違うのが分かりますね。
これで、prefix8ビット、公開鍵ハッシュ160ビット、checksum32ビット、で200ビットが得られた。これがバイナリ形式のアドレスってわけ。
バイナリ形式からBase58エンコードしてアドレスを得る
そうしたらこの200ビットのデータを、「200ビットの大きな整数」だと思って58進数で表したものがビットコインアドレスになるのよ。
58進数では数字が58種類必要なので、小さい順に記号
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
を割り当ててるのよ。手書きのときのミスをなくすため、数字10文字+大文字26文字+小文字26文字の合計62文字から、数字の0とアルファベットの大文字のI, O, 小文字lの4文字を使わないことで58文字にしてるわね。
**「ビットコインアドレスは1か3ではじまり、3はマルチシグ用」**というのを聞いたことがある人は多いと思うけど、これがその理由よ。200ビットの値といっても、先頭の8ビットはversion prefixのせいで0x00か0x05で固定されているから、先頭の桁は1か3になる、ってわけ。10進数でいえば、「0から9999までの数を、必ず4桁になるよう0000~9999で表記するとき、1000未満の数なら先頭には0がくる」というのと同じ理由よ。
もっとも、逆に先頭の文字が1種類に決まるようにversion prefixを定めた、というのが真の理由でしょうね! もっと長いprefixをつけて、Base58エンコード後に決まった文字列が先頭にくるように細工してる例はたくさんあるから。
(注)checksumの付加は本当はBase58Checkという仕様で定めているものだが、ここでは簡単のためchecksum込みでバイナリ形式のアドレス、と呼んでいる
実行例
C#での実行例よ。生の秘密鍵から出発して、公開鍵・公開鍵ハッシュ・ビットコインアドレスを導出しているするところに注意してね。
Network n = Network.Main;
HexEncoder enc = new HexEncoder();
Key private = new Key(enc.DecodeData("49973d744a37f67e0768952c35ab3022a0113c78ce48eb8e0c0157ea2c41e7d7"));
PubKey pub = private.PubKey;
Console.Out.WriteLine("pubkey: " + pub.ToHex());
Console.Out.WriteLine("key hash: " + pub.Hash.ToString());
Console.Out.WriteLine("address: " + pub.GetAddress(n));
(結果)
pubkey: 020756dfda36a0fc05d891fa1a9d02b8dda223f0621e1b90b5b579280cc44ec51d
key hash: e8755c455a44cd1a34aaba408f18c9d812e9657c
address: 1NC8MrL8UbfV1Sex7ZKh6Lwxomg4G9Q54Y
4997...
で始まる文字列が秘密鍵なので、どのコンピュータでもこれを実行すれば同じアドレスが得られるはずだけど、このアドレスに本当にビットコインを送ってはいけませんよ! このアドレスに対応する秘密鍵はここに書いてあるので、それを知っていれば誰でもそのビットコインをよそに送ることができてしまいます。
また、公開鍵が020756...
の33バイト、公開鍵ハッシュがe875...
の20バイトなのは後の説明で使うから大事よ。
一方、Network n = Network.Main
の部分をNetwork n = NBitcoin.Altcoins.Litecoin.Instance.Mainnet
やNetwork n = NBitcoin.Altcoins.BCash.Instance.Mainnet;
に換えればそれぞれライトコインとビットコインキャッシュのアドレスが得られるわよ。アドレスはハッシュを取ったものだから、このコード例からわかるように、
-
通貨の種別が違っても同一の秘密鍵が使える
-
異なる通貨のアドレスが同一の秘密鍵を使っていたとしても、そのことは外部からは一切わからない(異なるversion prefixでハッシュを取ってアドレスとするから)
のは大きなポイントよ。
トランザクションスクリプトを理解する
内蔵言語とスタックマシン
ビットコインにはトランザクションスクリプトという簡単なプログラム言語が内蔵されているわ。
「簡単な」というのはチューリング完全ではない、という意味で、一般のプログラム言語のような高度な動作をさせることはできない(悪意あるトランザクションからの防御のため)けど、そのぶんスタックマシンの勉強としてはちょうどいい感じよ。Javaも.NETもVMレベルでは実はスタックマシンだから、知らない人はこの際勉強しましょう!
これは適当な blockeplorer.comの適当に見つけたビットコイントランザクション から持ってきたスクリーンショットだけど、OP_DUP
で始まるScriptPubKeyという要素があるでしょう? これがいったい何かを説明するわね。
スタックマシンは、1つのスタックを持ちながらインストラクションを順番に実行する仮想的なマシンよ。スタックは基本的に先頭要素にしかアクセスできないので、そのぶんインストラクションの構造が簡単になれるの。
もっとも素朴な支払い方 P2PK
一番簡単な例で、相手の公開鍵に直接支払いをするスクリプトを紹介するわ。送金先向けのスクリプトはScriptPubKey, 送金元が過去のトランザクションで送られたビットコインを解除するスクリプトはŠcriptSigと歴史的理由で呼ばれているのよ。ScriptPubKeyといっても公開鍵そのものじゃあないの。
また、トランザクションでは送金元に関する部分をinput、 送金先に関する部分をoutputと呼ぶわよ。日本語の語感と逆だから注意してね。
PubKey pub = ... //上で得たのと同じ公開鍵
Console.Out.WriteLine("pk script: " + pub.ScriptPubKey.ToString());
(結果)
pk script: 020756dfda36a0fc05d891fa1a9d02b8dda223f0621e1b90b5b579280cc44ec51d OP_CHECKSIG
この02...1dは33バイトのデータで、先頭の02が「圧縮された公開鍵かつY座標が正」であることを示し、あとの32バイトが公開鍵のX座標本体、となってます。意味は公開鍵そのものね。ハッシュとかではないわよ!
- (注) この支払い方法はおそらく今は認められないはずですが、原理を理解するには適切なので紹介しています
最後のOP_CHECKSIGで、新しいトランザクションの発行者が最初に置いたsignatureと、過去のトランザクションのUTXOの公開鍵とが一致することが確認され、UTXOの正当な所有者だ、とわかるわけ。
トランザクションサイズを減らす努力
この形式だと公開鍵全部をトランザクションに載せる必要がある一方、アドレスから公開鍵を復元することはできないので不便です。各自が公開しているビットコインアドレスは公開鍵のハッシュであって、公開鍵そのものではないから。
またもう一つ忘れてはいけないのは、トランザクションのデータはすべてのビットコインマイナーが新ブロックを見つけるための競争のために数限りなくたくさんの回数のハッシュ演算にさらされる、ということよ。1ブロックあたりのデータ総量も上限があるから、トランザクションのデータを1バイトでも減らすことができれば、世界の消費電力やビットコイン相場にも影響があるでしょうね。
短縮形 P2PKH
そこで、公開鍵のハッシュに送る方法が基本になったわけ。これはビットコインの最初期からある機能だわ。P2PKHというのは、Pay to Public Key Hash よ。
PubKey pub = ... //上で得たのと同じ公開鍵
BitcoinPubKeyAddress p2pk = pub.GetAddress(n);
Console.Out.WriteLine("pubkey hash: " + pk.Hash.ToString());
Console.Out.WriteLine("p2pkh script: " + p2pk.ScriptPubKey.ToString());
(結果)
pubkey hash: e8755c455a44cd1a34aaba408f18c9d812e9657c
p2pkh script: OP_DUP OP_HASH160 e8755c455a44cd1a34aaba408f18c9d812e9657c OP_EQUALVERIFY OP_CHECKSIG
このケースだと、トランザクションを使いたい側(送金されたビットコインを使いたい側)は、あらかじめ「秘密鍵で署名した送金内容」「公開鍵本体」の2つをスタックに入れた状態でスタックマシンを走らせるわけよ。
また、各インストラクションの大ざっぱな意味としては以下のようよ。
インストラクション | 意味 |
---|---|
OP_DUP | スタックの先頭を複製する |
OP_HASH160 | スタックの先頭をその値の160ビットハッシュで置換する |
OP_EQUALVERIFY | スタックの先頭と2番目が等しいことを確認する。等しくなければトランザクション無効と判定 |
OP_CHECKSIG | スタックの先頭が公開鍵、2番目が署名、として検証する。検証失敗ならトランザクション無効と判定 |
OP_DUPとかのインストラクションはデータサイズとしては小さいから、定数としてトランザクションにハードコーディングされた値は33バイトから20バイトに縮小されているのがこのテクニックの効果よ。
横展開 P2SH の発明
でもこれだとスクリプトはトランザクションのinput側に置かれるから、特にマルチシグとかの長いスクリプトでは送金手数料が上がってしまう(トランザクションのデータサイズと手数料は比例するので、送り先がマルチシグだと送り元の負担が増すのはまずいのよね)という問題があった。
ビットコインがある程度できてきたあと、ある頭のいい人が「スクリプトの現物を提供する役割をinputに移し、outputにはスクリプトのハッシュを置く」工夫をすることでトランザクションサイズを減らせるテクニックを発明したの。これで手数料負担はマルチシグアドレスの保有者側に移った。これが**P2SH ( Pay to Script Hash )**よ。
PubKey pub = ... //上で得たのと同じ公開鍵
BitcoinScriptAddress p2sh = pub.GetScriptAddress(n);
Console.Out.WriteLine("scriptPubKey hash: " + pub.ScriptPubKey.Hash.ToString());
Console.Out.WriteLine("p2sh script: " + p2sh.ScriptPubKey.ToString());
>>scriptPubKey hash: 908d5be94c533e3b3b17fbdf73e41cd7696938b0
>>p2pkh script: OP_HASH160 908d5be94c533e3b3b17fbdf73e41cd7696938b0 OP_EQUAL
その前の例と比べると、アドレスの生成がpub.GetAddress(n)
からpub.GetScriptAddress(n)
へ変わっていることと、アドレスのスクリプトがpk.Hash
からpk.ScriptPubKey.Hash
に変わっていることに注目!
このトランザクションの受け手が、次回の送金時に先のP2PKで提示したのと同じスクリプト020756dfda36a0fc05d891fa1a9d02b8dda223f0621e1b90b5b579280cc44ec51d OP_CHECKSIG
を使うことを想定しているのね。pub.GetScriptAddress(n).ScriptPubKey
の中にpub.ScriptPubKey.Hash
が登場することがその証拠、ということよ。
ハードコーディングされた定数が160ビットなのはP2PKHのときと同じだけど、スクリプト本体はもっと長いものであってもinput側のトランザクションデータサイズはほとんど変わらないのが大きなところなのよ。
なぜP2SHではアドレスが違うか
でもそのかわり、P2SHの登場以前の古いビットコインソフトウェアは、input側でスクリプトを提供しなければならないことを知らないわけだから、うっかり古いソフトウェアで鍵が管理されているアドレスにP2SHで送ってしまうとそのビットコインは永久に失われてしまいかねない。トランザクションの正当性を検証するのは世界中のノードだから、個々のアドレスについてそれを管理するソフトウェアがどうなっているかは知る由もないわけ。それを緩和するために新しいversion prefixを用意して、3ではじまるアドレスを導入した、ってことよ。
アドレスの先頭文字だけで判定できるならトランザクション検証側も楽に判定できるからね。
トランザクションタイプ | 送り先アドレス | 送る際の注意 |
---|---|---|
P2PKH | 1で始まるアドレス | 古いソフトウェアで管理されているアドレスの可能性があるのでP2SHで送ってはならない |
P2SH | 3で始まるアドレス | P2SHをサポートしているソフトウェアで管理されたアドレスなことが保証されているのでP2SHで送ってよい |
となるわけ。
どう、これで1で始まるアドレスと3で始まるアドレスの違いがわかった?
ときどき、3で始まるアドレスはマルチシグ、と言われることがあるけどこれは不正確。スクリプトのハッシュであると言っているだけで、そのスクリプトがマルチシグだとは誰も言ってないからね。実際このサンプルソースはマルチシグじゃないけどP2SHとして機能するのよ。
ある意味開き直りの segwit の発明
さらに時代が下って、新しく出たのがsegwitよ。従来、署名データもトランザクションの中に入れていたのを「別送」にして無数のハッシュ演算の対象から外すことでマイニング負荷の軽減を図る、というのが大ざっぱな説明ね。もうちょっと詳しく知りたいという人は例えばこういう記事 があるわ。
PubKey pub = ... //上で得たのと同じ公開鍵
BitcoinWitPubKeyAddress segwit = pk.GetSegwitAddress(n);
Console.Out.WriteLine("segwit script: " + segwit.ScriptPubKey.ToString());
>>segwit script: 0 e8755c455a44cd1a34aaba408f18c9d812e9657c
このe875...
の値はP2PKHの説明で出てきたpub.Hash
の値と一致していることには注目に値するわね。(興味ある人は最初から丹念に読み返しましょう!)
トランザクションサイズを減らすためとはいえ、スクリプトとしてプログラムっぽい動作をすることは諦めて単なる記号列になってしまったのが当初の設計理念とは異なってきたけど、経済性優先なら仕方ないわね。
ビットコインキャッシュとライトコインについては、このアドレス生成まではsegwitでもできるけど、blockchain explorerとよばれる公共のビューワではsegwitは未対応のものがほとんどみたい。(2019年4月時点)
アドレスを見ただけでsegwitかどうか判定できる必要があるという理由はP2SHのときと同じよ。segwitではBase58ではなくBech32という別のエンコード仕様になっているのも相違点よ。
これまでの話をまとめると、アドレスの種類と何で始まるかの関係はこうなるわね。
コイン種類 | アドレス種類 | version prefix | アドレスの先頭 |
---|---|---|---|
ビットコイン | P2PKH | 0x00 | 1 |
ビットコイン | P2SH | 0x05 | 3 |
ビットコイン | segwit | (Bech32) | bc1 |
ライトコイン | P2PKH | 0x30 | L |
ライトコイン | P2SH | 0x32 | M |
ライトコイン | segwit | (Bech32) | ltc |
ビットコインキャッシュ | P2PKH | 0x00 | bitcoincash:q |
ビットコインキャッシュ | P2SH | 0x05 | bitcoincash:p |
ビットコインキャッシュ | segwit | (Bech32) | bch |
さいごに
私の推測だけど、このような中途半端なトランザクションスクリプトではなく、本格的なプログラミングができるような機構があるべきだ、という理念で作りなおしたのがイーサリアムを代表とするスマートコントラクトの機能を持った通貨よ。個人的にはこっちの方面がブロックチェーンの応用という観点では幅広く対応できると思うし、私の記事も今後はイーサリアムとスマートコントラクトがメインになると思うのでよろしくね!
後続の記事の方向性の希望もここへのコメントで受け付けます! こっそり私に連絡したい方は benten@lagarto.co.jp にメールちょうだいね。Twitterアカウントもよろしく! Twitterはまだ作ったばかりだけどね。
スマートコントラクトの開発・コンサルティングもやってるよ!