『ゼロから創る暗号通貨』
第4章: Wallet と Transaction : Transactionの中身について考える
濵津 誠/hamatz
本章では、暗号通貨を実現するための最後のピースであるWalletの実装を通じて、Transactionについて学びます。まずは、暗号通貨を成り立たせるための要素技術である、アドレスやUTXO(Unspent Transaction Output)について確認します。その後、Transactionの構造について学び、それらを取り込む形でWalletアプリケーションを完成させます。
次のような点に着目しながら読み進めてみてください。
「さて、ここまでの実装ではTransactionをきちんと定義せず、ダミーを使っていた。いよいよ、このTransactionに真剣に向き合うときがきたよ」
「おお!相手を指定して送金する部分と、Wallet、つまり財布に相当するアプリケーションを作るわけだね。暗号通貨の完成に向けた最後の1ピースを埋める感じだね」
「そうだね。ようやくゴールが見えてきた。しかし、それなりに複雑な部分だから、油断せずにいこう。今回は全体像と照らし合わせると図4.1で示す箇所になる」
「Transactionには、送り先と送り主と金額程度の情報を持たせればいいんだよね。あんまり難しいってイメージが湧かないんだけど……」
「そう思うのも仕方ないかもしれないね。たしかに、今までダミーとして使っていたTransactionのデータは、送り先と送り主と金額程度の情報しか持たない単純な構造をしている」
transaction = { 'sender': "test1", 'recipient': "test2", 'value' : 3 }
「え?それじゃダメなの?」
「ダメだね」
「ダミーで使っていたフォーマットのままだと、どういう問題があるんだろう?」
「まず、そもそもの話として、送り先について何も規定がない。これまでは、test1やtest2といった文字列を使って送り先を指定することにより適当に済ませてきた。SimpleBitcoin上で利用できる情報として、メールアドレスに相当するようなものを規定する必要がある」
「itsuki@simplebitcoin.com
みたいな、メールアドレスっぽい文字列を使うだけではダメなの?」
「ダメだ。それじゃあ、まずはそこから考えてみようか。なぜSimpleBitcoinで任意の文字列を使って自分を指し示すのではダメなのか?」
「りょーかい!」
「暗号通貨のキーワードは『トラストレス』だ。これまで何度も触れてきたから、そろそろ覚えたんじゃないかい?」
「うん。誰も無条件では信用するな、ということだよね」
「そう。Transactionについても、自分が送信したままの状態でTransactionが全Coreノードに到達し、そこでブロックに取り込んでもらえるか、という点を心配する必要がある」
「ブロックチェーンに改ざんできない仕組みが入っているとしても、そこに取り込まれる前の段階ですでに改ざんされていたら、何も保証してもらえないってことだよね?」
「そのとおり。それを回避するために、電子署名という技術が使われる」
「電子署名……」
「聞いたことくらいはあるかな?」
「んー、なんとなくは……」
「では、まずは電子署名についての説明から始めようか」
「よろしくお願いします!」
「図4.2を見てほしい。電子署名を成り立たせる要素技術は、公開鍵暗号方式とセキュアハッシュ関数だ」
「セキュアハッシュ関数ってのは、ブロック生成のところで使ったハッシュ関数と同じ?」
「そう。出力値を見ても、元の入力値となったデータを逆算できない関数のことだ」
「公開鍵暗号方式ってのは、はじめて出てきた」
「公開鍵暗号方式というのは、秘密鍵と公開鍵というペアを使うタイプの暗号化手法だ。公開鍵で暗号化したデータは、そのペアとなる秘密鍵でのみ復号可能という特性を持っている」
「名前に『公開』って付いているくらいだから、誰でも見られる鍵ってこと?」
「そうだね。他人に知られても問題のない情報として扱えるのが公開鍵だ」
「そんな情報を使うことで、何がどう嬉しいの?」
「たとえば、誰かに見られたくないデータを暗号化し、その暗号を復号できる鍵データを、誰か特別な人にだけ渡したいとする。その場合、その鍵データをどのような方法で相手に渡せばいいだろうか?」
「もしも鍵データを渡すときに誰かにそれを盗まれたら、鍵データを盗んだ人が暗号化データの中身も見られてしまうね」
「そこで公開鍵暗号方式を使う。相手の公開鍵を使って鍵データを暗号化し、それを相手に渡すんだ。それを元の鍵データに復元できるのは、ペアとなる秘密鍵を持っている相手だけになる」
「誰でも見られる場所に公開できる鍵だけど、それを使って暗号化したデータを元のデータに戻せるのは、公開鍵とペアになる秘密鍵の持ち主だけ、ということか」
「論より証拠。公開鍵暗号方式の動作をコードで見てみよう。次のコードは、公開鍵暗号としてよく知られているRSA方式を使った暗号化と復号を試すサンプルだ」
「公開鍵と秘密鍵のペアを作り、用意しておいたテキストをまず公開鍵を使って暗号化し、その結果を秘密鍵で復号して最後に突き合わせているわけだね」
「そのとおり。さっそく実行してみよう」
encrypto : (b'!\x1cn\x89x\xf1\x91\x11\x81\xf4\x8e\xf5L....',) decrypto : b'This is test message for getting understand about digital signature' test_txt and decrypto are same!
「なるほどー。公開鍵でよくわからないデータ列に変換されたテキストが、秘密鍵でちゃんと元に戻ってる。便利なものもあったもんだなー」
「そして、この公開鍵暗号方式の特性を逆に生かすのがデジタル署名だ」
「特性を逆に生かす?」
「そう。秘密鍵で暗号化したデータは、それとペアになる公開鍵で復号できる」
「でも、誰でも元のデータに戻せちゃうから、暗号化した意味がないよね?」
「何かを秘密にしようという用途では、たしかにそのとおりだ。だが、デジタル署名は何かを秘密にするための技術ではない」
「じゃあ何が目的なの?」
「現実世界では、たとえば誓約書で『たしかにこの内容に私は同意しました』という証拠として手書きのサインやハンコが要求されたりするよね」
「うん」
「それと同じように、何かのデータに対して、その内容が改ざんされておらず正しいものであることを自分が保証しますよという意味で、本人にしか作成できないデータをくっつける。その、本人にしか作成できないデータを秘密鍵による暗号化で作り出すというのが、デジタル署名の基本的な発想だ」
「秘密鍵で暗号化する前のデータを隠したいわけじゃなく、そうやって作ったデータをデジタル署名として、内容を保証したいデータと常にセットにするわけか」
「そのとおり。図4.3のように、『本当に秘密鍵の持ち主が暗号化したものならば、公開鍵を使って復号すれば元のデータと一緒になるハズだ』ということを確認するのが目的だ」
「ハッシュ関数のほうは、何で必要なの?」
「『入力値が少しでも異なれば出力値が異なる』というハッシュ関数の特性を利用すれば、動画や画像のような大きなデータでも簡単に素早く比較できるようになるから、便利だと思わないかい?」
「なるほどー」
「こうした特性を使って、暗号通貨のTransactionには改ざん防止のために署名がつけられる。それを受け取った相手では、元データが改ざんされていないことをチェックするために、公開鍵による署名検証が必要になる」
「改ざんされていない送金情報のみをブロックに取り込むためのメカニズム、ということだね」
「デジタル署名についても、ちょっと試してみようか?」
「試すって、どうやって?」
「さっきのサンプルコードで、暗号化と復号で使う鍵を逆にしてみるんだよ」
「ああ、なるほど。こうかな?」
hashed = SHA256.new(test_txt.encode('utf8')).digest() print("hashed :" , hashed) #秘密鍵で暗号化 enc_with_priv = privkey.encrypt(hashed, 0)[0] print("enc_with_priv :", enc_with_priv) # 公開鍵で復号 dec_with_pub = pubkey.decrypt(enc_with_priv) print("dec_with_pub :", dec_with_pub) if hashed == dec_with_pub: print("hashed and dec_with_pub are same!")
「いいだろう。実行してごらん」
「りょーかい……。あれれ?」
enc_with_priv : b'!\x1cn\x89x\xf1\x91\x11\x81\xf....' Traceback (most recent call last): <中略> File "/usr/local/lib/python3.6/site-packages/Crypto/PublicKey/RSA.py", ine 174, in decrypt return pubkey.pubkey.decrypt(self, ciphertext) File "/usr/local/lib/python3.6/site-packages/Crypto/PublicKey/pubkey.py", line 93, in decrypt plaintext=self._decrypt(ciphertext) File "/usr/local/lib/python3.6/site-packages/Crypto/PublicKey/RSA.py", line 239, in _decrypt mp = self.key._decrypt(cp) File "/usr/local/lib/python3.6/site-packages/Crypto/PublicKey/_slowmath.py", line 52, in _decrypt raise TypeError("No private key") TypeError: No private key
「RSAの場合、理屈の上では、秘密鍵で暗号化したものを公開鍵で復号できる。しかし、君が指摘したとおり、それでは情報の秘匿という観点では意味がない。また、普通のデータを秘密鍵で暗号化するだけだと、その元データと暗号文の組み合わせを収集することで、秘密鍵を推測する攻撃が可能になる。そのような問題もあるので、このとおり、ライブラリではエラーが出るようになってるんだ」
「えっ?じゃあダメじゃん。どうすんのデジタル署名?」
「安心していい。その辺の厄介な処理はライブラリ側でうまい具合に済ませてくれるようになってる」
# 与えられた秘密鍵を使ってメッセージに署名する def compute_digital_signature(message, private_key): # まずメッセージをハッシュ値に変換する hashed_message = SHA256.new(message.encode('utf8')) # 署名にはRSASSA-PKCS1-v1_5を利用する signer = PKCS1_v1_5.new(private_key) return binascii.hexlify(signer.sign(hashed_message)).decode('ascii') def verify_signature(message, signature, pub_key): hashed_message = SHA256.new(message.encode('utf8')) verifier = PKCS1_v1_5.new(pub_key) return verifier.verify(hashed_message, binascii.unhexlify(signature)) def main(): test_txt = "This is test message for getting understand about digital signature" pubkey, privkey = generate_rsa_key_pair() # デジタル署名を生成する signed = compute_digital_signature(test_txt, privkey) print("signed :", signed) # デジタル署名を検証する result = verify_signature(test_txt, signed, pubkey) print("result :" , result)
「おお。署名を作る命令だけでなく、署名を検証する命令まで抽象化されてる!」
「そしてこれがその実行結果だ」
signed : 5aa2e344f53aa883b43da774b94781168b9.... result : True
「なるほどー」
「興味があったら内部のメカニズムを詳しく調べてみるといい。今回はRSAの仕組みそのものにまで深入りはしないから、このまま進めるよ」
「うん。えっと、ここまでで、Transactionの正当性を示すためにデジタル署名が必要だってことはわかった。でも、自分を指し示す情報としてメールアドレスっぽい文字列を使うのではダメという話は、デジタル署名にどう関係してるの?」
「使うのがダメとは言っていない。無駄なんだ」
「無駄?」
「そう。さっき話したように、Transactionの正当性を確認するには、秘密鍵による署名を検証するため、それに対応した公開鍵を使う必要がある」
「うん。大丈夫。覚えてるよ」
「Transactionの中で宛先に使う情報が公開鍵そのものになっていることで、Transactionに付加されている署名の検証に必要な公開鍵を、そのまま取り出せるわけだ。メールアドレスっぽい文字列だと、そこから紐づけられた情報を辿って、どこからか公開鍵を取ってくる必要が生じてしまう」
「ああ、なるほど。それは効率が悪そうだね」
「調べてみると、実によく考えられた仕組みになっていることがわかるだろう。ここ、重要だからよく覚えておくように」
「うん。少なくとも、ネットワーク上で送金相手を指定する情報としては、公開鍵の情報そのものを直接利用するのがベスト、というのはわかったよ」
「いいね。実際の暗号通貨アプリケーションでは、電話帳アプリが電話番号やメールアドレスが誰に対応しているかを管理しているように、どの公開鍵が誰に対応しているかは別途管理する必要はあるだろう。それでも、そのような情報をTransactionに持たせる必要はない」
「送信先として指定する文字列だけでなく、差し出し元と金額の部分もこれまではダミーを入れていたよね」
「差し出し元については、説明するまでもなく、送信先と同様に公開鍵の値を使う。だから、残っている未解決の課題は、金額の宣言方法だね」
「数値を宣言するだけでは不十分ってこと?」
「そう。たとえTransactionにデジタル署名がついていたとしても、それは、その本人が間違いなく作成したものであることを保証してくれるだけだ。中身が正しいかどうかについては、デジタル署名では保証されない」
「えっ?どういうこと?」
「リアルな世界に置き換えて考えてみよう。君は、自分が持ってもいないお金を人に渡すことができるかい?」
「ただの口約束ならともかく、ないものを渡すことはできないよ」
「しかし、今まで使っていたダミーは、送付するポイントとして単なる数字を格納してるだけだね?」
「あっ!」
「そう。今のままだと、いくらでも、誰に対しても、ポイントを送り放題だ」
「ポイントは、有限だからこそ、もらえることに価値があるんだったね……。忘れてたよ、兄さん!」
「そのとおりだ。暗号通貨におけるTransactionデータは、次の2つの性質を満たすことで、はじめて意味があるといえるだろう」
「なるほど……。でも、本人が一定の額を持ってるかどうかなんて、一体どうやって確認するの?」
「そのための鍵となるアイデアがUTXO(Unspent Transaction Output)だ」
「UTXO?」
「そう。直訳すると、『まだ使われていないTransactionの出力』。ものすごく簡単にいえば、自分が他人から受け取った額を出どころとしてのみ新しいTransactionを作れる、という考え方だ。図4.4を見てほしい」
「丸の中の数字が額を表現していて、そのまま使ったり組み合わせて額を増やしたりしながら送ってる感じ?」
「誰かに対する送金情報(Transaction Output)が、そのまま別の誰かに対するInputとして利用される。そういうイメージだと思ってくれればいい」
「えっと……?」
「大事なところだから、少し丁寧に説明しよう。図4.5を見てほしい。TransactionにはInputとOutputという2つの側面があって、Inputに入れることができるのは、それ以前に作成されたTransactionに含まれるOutputだけだ」
「ふむふむ」
「ただ、図4.4にもあるとおり、Inputに使える過去のOutputは1つだけとは限らないという点に注意してほしい」
「なるほど……。図4.5の点線で描かれた丸は何を意味してるの?」
「自分に返金する分、つまり、お釣りだね」
「えっ?自分で自分にお釣り?」
「送信する額のベースとなるInputが大きすぎる場合に、もう一度自分で自分宛のOutputを作る必要があるんだよ」
「なんでそんなめんどくさいことを?」
「ベースになるInputのうちこれだけしか使われてないから、次にそこからいくらまでなら使えるか、なんていう計算を後でするほうが、はるかにめんどくさいよね?」
「いわれてみると、たしかにそうか……。Inputに指定されている内容のうち何割がもう使用済みかってことを、後になって確認しようとするようなもんだしな」
「それじゃあ、ここまでの話が理解できたかどうか、まとめてみよう」
「うん。今回のポイントは次の4点になると思う」
「いいだろう。それでは、ここまで触れずにきたTransactionに関する最後のポイントを確認することにしよう」
「最後のポイント?」
「あるInputは必ず何らかのOutputをベースにしている、という点は理解したね?」
「うん」
「では、大元となる最初のOutputは、いったいどこからやってくるのだろう?」
「それ、気になってた」
「気になるだろう?SimpleBitcoinのネットワークが最初に立ち上がった瞬間は、誰かから誰かへのTransactionを作ろうにも、まだ誰もOutputを受け取っていない」
「だよね。暗号通貨の最初の最初って、Outputはどこからやってくるんだろ?」
「理屈の上では複数の方法が考えられる。たとえば、最初のブロックを作るタイミングで誰かに対して莫大なコインが割り当てられるようなOutputを生成し、その人がネットワークの参加者たちに配布する、といった方法も考えられなくはない」
「それだと最初の人が有利すぎるし、人が集めにくい感じがするけど、たしかに理屈の上ではそういうやり方もあるよなあ」
「そうだね。『こいつスゲー!』の投票券として暗号通貨を利用しようというSimpleBitcoinが目指しているスタイルだと、この方法では人が集めにくいだろう」
「最初から圧倒的強者がいる状態になっちゃうもんね」
「だから、SimpleBitcoinでは、ビットコインで採用されているCoinbase Transactionという手法を使うことにしよう」
「Coinbase Transaction?」
「簡単にいうと、ブロックを生成する際に新しく自分のコインを生み出すことを許す方法だ」
「自分のコインを生み出す?」
「そう。ビットコインをはじめ、よく知られている暗号通貨では、ブロック生成時に自分を持ち主とする新規の暗号通貨を埋め込むことが許されているんだ。このときに使う特殊なTransactionは、Coinbase Transactionと呼ばれている」
「なるほど。それが最初のOutputになって、そこから流通が始まるわけだ……」
「無から有を生み出す錬金術みたいだと思わないかい?」
「そりゃそうだよなー。支払い元が存在しないのに受取人がいるってことだもんなー」
「実際、『ブロック生成のために時間のかかる処理』を頑張ることで新しく自分の暗号通貨を得る、という一連のプロセスは、金の採掘に喩えて『マイニング』なんて呼ばれることもある」
「なるほどー。でも、金なら埋蔵量に限りがあるから価値があるのはわかるんだけど、暗号通貨はいくらでも湧き出てくるよね?その辺はどうなってるの?」
「Ethereumのように上限が設定されていないものもあるが、ビットコインを含め、発行総数に上限がある暗号通貨は多い。それによる稀少性があるからこそ、他人と競争してでも獲得しようという動機になるのだろうね」
「まさに金の採掘ってわけだ。じゃあ、発行総数の上限に達してしまった後は、ブロックを生成するモチベーションが低下するのかな?」
「実際にそのような状況を懸念する声はある」
「心配してない人もいるってこと?」
「ビットコインなどには『半減期』という考え方があって、全体で生成されたブロックの総数に応じ、報酬として自分自身に発行可能な暗号通貨の量を徐々に減らしていくことになっている」
「初期にブロック生成に参加したほうがメリットが大きいよ、みたいな言い方で人を集めようとしてたってこと?」
「そう。テクノロジーの進歩に応じて計算力も高まるから、ブロック生成のための計算コストも減っていくはずだ、という考え方も背景にあったみたいだね」
「それと発行総数に上限がある話はどう関係するの?」
「報酬が半減しても、実際にビットコインの運用に支障の出るような状況に至るほどには、マイナーと呼ばれるブロック生成者が減少しているという話は聞かない。最終的に手数料だけになるころには、それはそれでうまく回るだろうという意見もあるんだ」
「ビットコインくらいになると、市場価格が高くなり続けてたから、1回のブロック生成でもらえるコインの量が減っても、実質的にマイナーが得られる収入がたまたま大きく減らなかったっていう気もするけど」
「そういう側面も否定はできないね。正直なところ、ビットコインの市場価格がこの先どうなるのかはよくわからない。発行総数の上限に達した後でどうなるかもよくわからない。そのときになってみないと実際に何が起きるかはわからないだろう」
「まぁ、経済の話になってきちゃったし、SimpleBitcoinに話を戻そうよ」
「そうだね。ここでのポイントは、『SimpleBitcoinでは、TransactionのInputとして最初に使われる暗号通貨は、Coinbase Transactionとして生み出される』という点だ」
「そこまでは理解できたよ、兄さん。図にすると図4.6みたいになるってことだよね」
「そうだ。それでは、この理解内容をベースに、暗号通貨におけるTransactionについてもう少し深いところを見ていくことにしよう」
「りょーかい!」
「そろそろ忘れているかもしれないが、暗号通貨においてTransactionはブロックの中に格納される、という点を意識しておく必要がある」
「ああ、そう言われてみれば、1つのブロックに入っているのが1つのTransactionとは限らないんだったね」
「そう。ブロックには複数のTransactionを含めることができる。ほかに注意しておくべきことはないかな?」
「なんだろ……?」
「SimpleBitcoinにブロック生成のためのCoreノードを提供する人はボランティアではない、という前提だ」
「そういえば、第1章で、Transactionからブロックを生成してもらうのにサーバーに手数料を払うっていう話があったね」
「そのとおり。税金とか手数料みたいなものを考える必要がある」
「あれ?でも、ブロックを作るとCoinbase Transactionを使って自分宛のOutputを作り出せるんだから、すでにそれが税金みたいなものじゃないの?」
「そういう考え方もできる。しかし、ブロック生成のインセンティブが無制限に発行可能なもので支払われるならともかく、手数料はユーザー側から回収可能なものであるべきだ。ビットコインでもそうなっている」
「そういえば、ビットコインには半減期があるから、将来的にはインセンティブがゼロになって、ブロック生成は手数料がもらえるだけの世界へ移行するんだっけ……」
「Transactionを生成する時点では、誰がそのTransactionをブロックに取り込んでくれるのかわからない。それでも手数料が間違いなくブロック生成者に支払われるようにするためには、一体どうすればいいだろう?」
「うーん。Transactionで設定する送付先アドレスは、事前には知りようがないのか……。どうするんだろ?」
「一番シンプルなのは、Transactionを受け取ったときに計算する方法だろうね」
「計算?手数料を?ブロックを生成する側が?」
「図4.7を見てほしい」
「なるほど。複数のTransactionでそれぞれにInputとOutputの合計値の差を計算して、それを手数料としてCoinbase Transactionに入れてしまうのか」
「図4.8を見るとわかるように、他のTransactionと一緒にブロックの中に格納されるので、ブロック生成者が無秩序に好き勝手な値を手数料として上乗せしても、メインチェーンに格納されるときに不正があれば検出される。要件を満たすにはこれで十分だろう」
「ポイントをまとめると、次の3つってことだよね?」
「そうだ。ここまででTransactionの構造もずいぶん見えてきたことだし、そろそろコードを書いていくことにしよう」
「よーし。クライマックスが近づいてきたぞー!」