🕸️

【夏休みの自由研究】そうだブロックチェーンを作ろう!

2022/07/26に公開

はじめに

ITニュースの方でも話しましたが、ここ数日、例の本でWeb3がとても話題ですね。まあ、炎上なので良い事では無いのですが、せっかくなのでその根幹技術らしいブロックチェーン周りの自分の理解を整理してみることにしました。とりあえず、簡易なブロックチェーンを作ってNFTスマートコントラクトについても少し考えていきたいと思います。
なお、ネタでは無く詳しくない分野なので勘違いとかあると指摘してもらえると嬉しいです。

注意

  • あくまで私の理解のアウトプットなので実際の挙動や仕様とは異なる可能性があります
  • 実践的で本格的な仕様や実装ではなく、あくまで基本的な理解のためのサブセットの作成
  • データの自由化!とかそういう話はしません

ブロックチェーンを作ろう!

ハッシュ値による改ざんの困難性

そもそもブロックチェーンとは何でしょうか? 一般には改ざんに強いP2Pな台帳という理解かな、と思います。暗号通貨として有名なビットコインを実装するために使われていることでも有名ですね。

Wikipediaによると、ブロック(≒レコード)という単位で管理されており、台帳は連続したブロックのチェーンとなります。各ブロックは前のブロックの暗号化ハッシュ 、タイムスタンプ、トランザクションデータを持ちます。これによって改ざんされてないことを保証するわけです。うん、良くわかりませんね!

ポイントに絞って言うと、一つ前のハッシュ値を持つことで途中のデータ改ざんされてないことが確認出来るということ、のはず。

ref: Pythonでブロックチェーンを実装して採掘までやってみたので解説する

という分けで論よりコードということで、実際にサンプルコードを書いてみます。以下のコードは単純にひとつ前のハッシュ値を含めて連鎖的にハッシュを求めているコードです。

irb(main):169:0> h = Digest::SHA256.hexdigest({data: 123, hash:"root"}.to_s)
=> "4b088e66b8405bd9ad9524e7ee9165b25aff462ca6f3056583329345b40a6e4f"
irb(main):170:0> h = Digest::SHA256.hexdigest({data: 456, hash:h}.to_s)
=> "811e8350d846dad9d1fb05d0ac0a8e4bc916e64f08a19578f560c16798ad45b2"
irb(main):171:0> h = Digest::SHA256.hexdigest({data: 789, hash:h}.to_s)
=> "9d1b8be5f0b577130b1b21d6f8748fe77c301f17de326be2982000ec596ec330"

例えばこのコードの2番目の値を456から999に変更してみましょう。

irb(main):176:0> h = Digest::SHA256.hexdigest({data: 123, hash:"root"}.to_s)
=> "4b088e66b8405bd9ad9524e7ee9165b25aff462ca6f3056583329345b40a6e4f"
irb(main):177:0> h = Digest::SHA256.hexdigest({data: 999, hash:h}.to_s)
=> "1e81c7537ccbcd05c41beac4c683b231ab42efc2ab67a5b3f819cbe325de4961"
irb(main):178:0> h = Digest::SHA256.hexdigest({data: 789, hash:h}.to_s)
=> "f073f24bbaae8d51f35c70149d193b28c99684ff5bcaca8f62797e7a568de505"

当たり前ですが、最終的に含まれるハッシュ値は例えデータが789と同じであっても変わってしまいました。このように途中だけ変えてもそれ以降のすべての値再計算してやる必要があるので、一部だけの変更が困難なのがこのデータ構造の特徴です。

マイニングと分散合意とnonce

暗号通貨といえばマイニングですよね! 目指せ億り人。ただ、これって実際何してるのでしょうか?
これはブロックチェーンの種類によって異なる部分もあるかもしれませんが、基本的には分散合意をしています。

まず、先程ブロックチェーンを改ざんするのは難しい、という話をしましたが、実は改ざんするだけなら簡単なのです。先程、途中の値を999にすることは出来ましたよね? 単に最終的なハッシュ値が異なるだけです。なので、違う台帳になるだけで台帳自体の改ざんが出来ないわけではありません。よって自分の好きな取引履歴を主張すること自体は出来ます。ただ、あるネットワークの中適切な取引履歴認められないだけです。この不正な取引弾く仕組み、言い換えれば複数のコンピュータ間正しい取引合意する仕組みが分散アルゴリズムです。別にブロックチェーン特有の概念ではなく、2層コミットとかPaxosとか古くて新しい分散システムとは切っても切れない話題です。ビットコイン等で使われている合意形成アルゴリズムはビザンチン将軍問題とかにも対応できるとか聞いたことがある気もしますが、この辺は今回は踏み込みません(というか私には踏み込めません)。

閑話休題。マイニングの話に戻ります。信頼できる承認者がいる一般的なC/S型のアーキテクチャと違い、非中央集権で正しさを決める必要があります。ブロックチェーンといっても種類が色々ありますが、例えばビットコインではPoW(Proof of Work)が使われます。トランザクションが発生した時に、新規のブロックをマイニングし割り当ててるのですが、不正を防止するためにマイニングによって得られる報酬が不正によるメリットより大きい必要があります。単なるハッシュ関数の実行では一瞬で結果が出てしまいますので、意図的に適度に計算を重くして過去履歴の再計算をした場合のマシンパワー、つまり電力消費を増やしています。この計算を重くするパズルは何でも良いのですが、適度に求めるのが重くて、かつ検証がしやすいという特性が求められます。そこでよく使われる手法がnonceを活用したものです。

nonce(number used once)とは「一度だけ使われる数」という意味です。主に元の値の後ろにくっつけてハッシュ化することで、認証だったりセキュリティ周りで使うことが多いですが、ブロックチェーンではこいつを使って例えば 「ハッシュ値の先頭4桁が0になるnonce」 を見つけます。ハッシュ関数は逆引きは出来ませんから、nonceを1つずつインクリメントして総当りで結果を求めるしかありません。一方で、検証は実際にハッシュ関数に適用するだけなので簡単に住みます。実際にコードを書いてみましょう。

h = Digest::SHA256.hexdigest("hello")
nonce = 0

while true do
    nonce += 1 
    calced = Digest::SHA256.hexdigest(h+nonce.to_s)
    if (calced[0..3] == '0000')
        break
    end
end

こちらを実行すると以下のような結果になります。

irb(main):189:0> h
=> "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
irb(main):190:0> nonce
=> 18617

ではこのnanceが正しいか検証してみましょう。

irb(main):193:0> Digest::SHA256.hexdigest(h+nonce.to_s)
=> "000075ed72b3e9b3efb94b739906d4c6bc58b1bb24a5e1fcbfa57feaba1558a8"

先頭の4桁が0であることがわかりますね? 4桁だと瞬時に終わると思いますがこれを5桁や6桁と桁数を増やしていくことで確率はどんどん下がるので負荷も増大します。私の環境だと5桁だと3秒弱、6桁だとおよそ30秒になりました。これがマイニングの正体です。通常は速さを求めてなんぼのアルゴリズムで、意図的に遅くしてるってのは面白いですよね! とはいえ、この方法にも課題があったり他のマイニング手法もあるのであくまで一例として、です。ここら辺はPublicとPrivateでも大きく違うでしょうし、トランザクション性能に直結するから工夫のし甲斐のあるところでしょう。

簡易なブロックチェーンを実装

さて、先程説明した要素をベースに簡易的なブロックチェーンを実装してみましょう。まあ、分散環境での実行では無いので合意とか重要な点も省いてますが。

def mining hash
    nonce = 0
    diff = 4
    while true do
        calced = Digest::SHA256.hexdigest(hash + nonce.to_s)
       break  if (calced[0..(diff-1)] == '0'*diff)
        nonce += 1 
    end
    nonce
end

def record(history, transaction)
    prev_hash = history.last.keys.first.to_s
    created_at = Time.now.to_s
    hash = Digest::SHA256.hexdigest(transaction + prev_hash + created_at)
    nonce = mining hash
    history + [{hash => {transaction:transaction, prev_hash:prev_hash, nonce:nonce, created_at:created_at}}]
end

def verify history
    diff = 4
    (1..(history.size - 1)).map { |i|
        prev_hash = history[i-1].keys.first.to_s
        hash = history[i].keys.first.to_s
        block = history[i].values.first
        created_at = block[:created_at]
        transaction = block[:transaction]
        return false if !(hash == Digest::SHA256.hexdigest(transaction + prev_hash + created_at))

        nonce = block[:nonce]
        return false if !(Digest::SHA256.hexdigest(hash + nonce.to_s)[0..(diff-1)] == '0'*diff)

        return true
    }.all?
end

history = [{ Digest::SHA256.hexdigest("root") => {}}]
history = record history, "Pay 100 yen from Nanoha to Vivio"
history = record history, "Pay 300 yen from Fate to Vivio"
history = record history, "Pay 500 yen from Vivio to Einhard"

verify history

さて、これで改ざんしにくいブロックチェーンを作ることが出来ましたね! もちろん、仮想通貨を始めとした実用的なブロックチェーンはもっと複雑なしくみだと思いますが、こうした基本的な振舞いを理解しておくことはそのアーキテクチャの特性を理解するためにとても重要だと思います。

ところで、そもそも暗号通貨はなんでお金になるの?

ところで、単なるハッシュ値取引の履歴でしかないブロックチェーンが何故お金として使えるのか、と不思議に思いませんか? これはそもそも 「お金って何ですか?」 という話になります。そもそも円とかドルとかなんで価値があるんですかね? 金(ゴールド) に交換出来るから? でもゴールドに交換出来たからってなんだというのでしょうか。あんなピカピカして伝導率の良い腐食しないだけの金属で… いや、割と有用なのですが少なくともお腹は膨れない。というか金本位制は日本やアメリカでは過去のものですし。それでも円やドルには価値があります。答えは簡単で 「なにか商品を購入する時に使える」と思ってる からとなります。所謂、信用という奴でサピエンス全史で言うところの虚構。通貨というのものは人類の多くが 「何かと交換できる価値がある」 と思っているという共同幻想によって成り立っています。まあ、詳しくはそれ系の本やサイトを読むのが良いと思います。漫画だと、ハイパーインフレーションとか狼と香辛料とかも良いかも。

で、ビットコインのような暗号通貨もこれと同じなんです。ビットコインで何か価値があるものが手に入ると多くの人が思っているから価値があるのです。もう少し言えばドルといった法定通貨と交換が出来るからです。ビットコインが直接使える場面はとても少ないですからね。そして、ブロックチェーンを不正にコピーする事も独自に作ることも簡単ですが、交換所所属するネットワーク正当な取引と認められたものでないと、当然交換所はドル交換しません。だからそういったものに価値は無いのです。このように暗号通貨価値を持つにはブロックチェーンでも何でもないふつうのアプリケーションである交換所正当な取引履歴と認められる必要があります。

NFTを実装してみる

NFTとは?

暗号通貨以外でブロックチェーンで最近話題の話としては。NFT (Non-Fungible Token) がありますね。件のWeb3とかに絡められてることも多い印象。日本語だと 「非代替性トークン」 ですが、ちょっと私はトークンの明確な定義が把握できて無いです。仮想通貨を始めとした仮想資産を表現するためのブロックチェーン上で使うために定義された仕様I/F だとは思うのですが定義っぽいものが見つけれなかったです。標準化された仕様としてはERC-20とかERC-721などがあるようです。

通貨や金融資産っぽい振舞いをするERC-20同じトークンの存在許容されます。金のインゴットは同じものがいくらあっても良い、とかのイメージでしょうか。一方でERC-721、つまりNFT非代替性の名の通り同じトークンの存在許容されません。なのでイラストとか楽曲唯一性を保証するために使われる、という事のようです。

簡易なNFTを実装してみる

例のごとくERC-721とかちゃんと読んでないのでなんちゃって実装ですが、先ほどのブロックチェーンの簡易実装をベースにNFTというかコンテンツを保持する仕組みを考えてみましょう。
この画像を例えばブロックチェーン上に記録するとします。

まずはトークンのデータ構造を考えます。例えば仮にこんなデータ構造だとします。

token = {
    key: '16dd997576ccf838d3fb05a5194da43c171531440579642db661a40960ae3bda',
    url: 'https://storage.googleapis.com/zenn-user-upload/cf4d618e266a-20220724.png'
}

画像や音楽を保存したGCSのURLをトークンに持たせます。トークンキーはURLをハッシュ化したものです。こちらを作成するために、まず以下のようにトークンの発行命令を記帳します。

history = record history, "mk_token https://storage.googleapis.com/zenn-user-upload/cf4d618e266a-20220724.png}"

そして、今までは記帳しかしてないのですが、実際にはビットコインだったらお金の送金だったり台帳に書かれな内容に応じた振舞いをアプリケーションがしないといけないはずです。という分けでreciveという記帳毎に動くイベントを作って、その中でユニークなトークンを発行する発行するmk_tokenというメソッドを実装します。

def recive history
    block = history.last.values.first

    transaction = block[:transaction]
    if transaction =~ /^mk_token /
        r = mk_token transaction
        history = record history, "mk_token: #{r}"
    end
end

@tokens = []
def mk_token transaction
    url = transaction.split(' ')[1]
    hash = Digest::SHA256.hexdigest(url)
    if @tokens.find_all{|x| x[:key] == hash}.empty?
        @tokens <<  {
            key: hash,
            url: url
        }
        "success"
    else
        "fail"
    end
end

さて、では実際に試してみましょう。recordメソッドを実行した後にreciveメソッドを都度実行します。2回同じURLを登録しようとするとちゃんと失敗するのでNFTとしてトークンの唯一性が保証されていますね。どんなトークンを作成したかはブロックチェーンに記載されるので改ざんは困難です。

irb(main):144:0> history = record history, "mk_token https://storage.googleapis.com/zenn-user-upload/cf4d618e266a-2022024.png"
irb(main):145:0> recive(history).last
=> {"2bc05aa766087d3e4bec4d9e451478737f893058a12d6f684baf71159e15eea2"=>{:transaction=>"mk_token: success", :prev_hash=>"f02d4ab31ae2acfcd691198e002cf1b1a6d485c7dbb34678719bb315b929499c", :nonce=>2870, :created_at=>"2022-07-25 00:42:01 +0900"}}
irb(main):146:0> history = record history, "mk_token https://storage.googleapis.com/zenn-user-upload/cf4d618e266a-2022024.png}"
irb(main):147:0> recive(history).last
=> {"3085940e82196fc57c7af8d4c5e86e9eab378e6466f8b0fda3d08f2ee08d108b"=>{:transaction=>"mk_token: fail", :prev_hash=>"74e55e6711bc6ce43ffc73c6326bf45eda0e1dfeb86cd81517daba8f25d7cd82", :nonce=>56592, :created_at=>"2022-07-25 00:42:11 +0900"}}

続いてNFTの所有者に関してですが、これは普通に台帳に取引履歴を記帳すれば良いですよね。

history = record history, "buy_nft 100 yen from Fate to Hayate, token: 078ce6c762673aa28a3149d2d8368b9c7acd362708eec06f821a595f62ff2fcd"
history = record history, "buy_nft 999 yen from Hayate to Signum, token: 078ce6c762673aa28a3149d2d8368b9c7acd362708eec06f821a595f62ff2fcd"

NFTというと他にも購入された金額の分配金なんかがビジネス的にはポイントなんだと思いますが、それもreciveメソッドを拡張して対応する処理を書いてやれば出来るはずです。

実際にはトークン周りはもっとリッチな仕様だと思いますが、エッセンスを突き詰めて簡略化するとNFTはこんな感じになるんじゃないかなー、と思います。

NFTとデータの改ざん

さて、素朴な実装を作ったのでこれを基にNFTについてもう少し考えていきましょう。まずブロックチェーンという特性上、何をどう頑張ってもNFTで保証出来るのはトークン自体記述されたデータ取引履歴の正しさのみです。これが何故保証出来るかは上のサンプルからも分かりますよね? 一方で、上記のサンプルでは画像楽曲と言ったコンテンツそのもののユニーク性は保証されません。だってトークンに記載されているのはURLですからね? URLではなくシステム内部の参照値でも同じですが、コンテンツのデータ自体ブロックチェーンに乗ってないのでコンテンツは保証出来ません

それでは、データ自体をブロックチェーンに書けば良いという話ももちろんあります。良いアイデアかもしれません。早速やってみましょう。 例えば先ほど画像をbase64で変換し、そのデータをトークンに直接書き込む以下のようなサンプルを書いてみます。なお、base64に変換されたバイナリはとても長いので、紙面の都合で表記を省略しています。

irb(main):164:0> history = record history, "mk_token iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAB/OSURBVG..."
irb(main):165:0> recive(history).last
=> {"7893ed9d96a2c388e264ac9d54e99be81382c7458182c693617117fe93d8bdc2"=>{:transaction=>"mk_token: success", :prev_hash=>"446677e56b00c42fe9032fd0569d881a59094573f3c44613be99ccb233f5c64f", :nonce=>38548, :created_at=>"2022-07-25 22:55:04 +0900"}} 
irb(main):166:0> history = record history, "mk_token iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAB/OSURBVG..."
irb(main):167:0> recive(history).last
=> {"6c50d232bccd08ebcb23adb11971d109a13a8872281120920505dd2848d2a2d0"=>{:transaction=>"mk_token: fail", :prev_hash=>"d21b83e95df71287b33e35ac9869112bcdf2696722eafdc231d8e1cee2e98a64", :nonce=>137278, :created_at=>"2022-07-25 22:55:05 +0900"}}

URLの代わりにdataとしてトークンの値として持たせています。ちゃんと2回目には失敗しているので、これでコンテンツそのものの唯一性が保証出来ましたね!
ところで、こちらのもう一つの実行結果をみてください。こいつをどう思う? 2回とも成功していますね。ちょっと見づらいのですが書き込んでるデータの後ろのあたりが/OSURBVG.../XSURBVG..ということで少し異なってるんですよね。これは異なるデータなので別のトークンとして保存されるのはNFTとして自然ですよね?

irb(main):174:0> history = record history, "mk_token iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAB/OSURBVG..."
irb(main):175:0> recive(history).last
=> {"559adbd6727d4e5e508f840491c57bfdf6c29f818485dfe053e9495c915166f6"=>{:transaction=>"mk_token: success", :prev_hash=>"345c9edc9f98eaf5be634b05701ed291a10283c90fcfbc09a47185cd97dda00c", :nonce=>69592, :created_at=>"2022-07-25 23:06:35 +0900"}} 
irb(main):176:0> history = record history, "mk_token iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAB/XSURBVG..."
irb(main):177:0> recive(history).last
=> {"e0dc4344e0647e5265701461f60735964c6bde4596bcac9ea5209c413e55b5b6"=>{:transaction=>"mk_token: success", :prev_hash=>"ac9b75ad91f93831d964b03be812646bcb45f445e1171178a10c2f4e72e2fca2", :nonce=>190812, :created_at=>"2022-07-25 23:06:35 +0900"}}

それでは二つのバイナリの画像を並べて表示してみましょう。あれ、同じ画像みえますね???


ぱっと見同じに見えますが、これは実は数ドットほど違う色に変更してあります。人間の目では区別が困難ですがコンピュータには別の画像に見えるのです。このあたり、NFTの課題としてよく言われる話なのですが実際に書いてみると分かりやすいですね。

もうひとつ、ブロックチェーンは改ざん複製自体が困難なのでは無く、改ざんしたものを所属するネットワークの中で正統なデータであると合意させるのが難しい、という事を思い出してください。暗号通貨のような場合はデータ自体に価値がありません。そのため取引所を通過できない不正なブロックチェーンを所有するモチベーションが無いのですが、マルチメディアの場合はデータ自体に価値があるというのが大きな違いです。つまりブロックを不正コピーするモチベーションがあるです。ネットワークの外ではNFTの唯一性は保証されませんから、このあたりもNFTの課題の一つですね。

このようにNFTでイラスト楽曲所有を管理するのは中々に困難です。一応ネットワークに所属していれば良いのでゲーム内コンテンツとかであれば有効に機能します。ただ、その場合はサーバサイドでふつうに実装すれば良いのでは? という話にもなりがちなのですし、ゲームを超えてポータブルなアイテムを作るなら、その仕組みの方が大変です。面白そうだけど課題も多い部分ですね。

あるいは

という考え方もあります。「以前、○○さんが持っていた」という事自体に価値があるものって実際としてはありますよね? 上手く権威になるようなサービスが出来ればそういった所有者の履歴を保証すること自体の価値は生まれるのかもしれません。暗号通貨のように保証書業界とか既存の共同幻想の中に入り込めるか次第ですね。

スマートコントラクト

スマートコントラクトは1994年に提唱された概念で、契約のスムーズな検証、執行、実行、交渉を意図したコンピュータプロトコルとのことです。ようは報酬の支払いを含めて自律的に動くリアクティブシステムだと思います。ブロックチェーン特有の概念では無いようですが、ブロックチェーンの用途の一つとして注目されています。もっとも有名なのがEthereumですね。

厳密には異なるのかもしれませんが、Dappsやブロックチェーン3.0も同じ類の話だと理解しています。ようはブロックチェーンを入出力として記載された命令を実行するプロセッサとなるアプリケーションを実装して動かす、という話だと思います。その上で実行される場所が自分のローカルマシンでは無く、報酬に応じたノードで動くので分散/Decentralizedと呼ばれるのかな、と。

さてこの仕組みってどっかで見ましたよね? 実は先ほど作ったNFTのrecivemk_tokenが実は同様の仕組みです。なのでNFTスマートコントラクトによって実装されたアプリケーションの1種とされるのですね。

ところで、今回作成したNFTでは命令を直接ブロックチェーンに書いてそれを読み取って処理していました。ただ、実際にはブロックチェーンの上に仮想マシンを作成し、そこに対してプログラミングを行うようです。ブロックチェーンに直接的に命令を書くのはアセンブラを書くようなものなのかもしれません。仮想マシン自体はそれで記述してるはずですが。Ethereumでは EVM(Ethereum Virtual Machine) というチューリング完全な仮想マシンを使うようです。なので、あの謎レイヤー図でブロックチェーンの上に仮想マシンが乗ってたのは正しかったようですね。VMWare的なものでは無くJVM的なもので、かつそれより上が合ってたのかは不明ですが... チューリング完全ならばエミュレータとから実装すればLinuxのようなOSを作る事も理論上は出来るはず。性能? 用途?それはまた別のお話ですね…

オープンな台帳を元に、透明性が高くDecentralizedな仕組みを実装するのには良さそうです。削除がしづらいので現在のフェイクニュースや誹謗中傷の流れも考えると、ライバル企業同士のB2Bとか運営主体を政治的に作りたくない特定のユースケースとかで役立ちそうですね。とはいえ原理的にトランザクション数を捌くのは厳しそうな気もしますが…

まとめ

とりあえずWeb3が炎上してたので、ふと思い立ってブロックチェーンのサブセット的なものを実装してみました。これら自体は単なるオモチャですが、やっぱり何かしら作ってみると実際の動きの予想が付くようになって良いですね。あと、個人的にはNFTは(投機的な話をわきに置けば)目指してる世界観は結構好きなのですよ。透明性の高い分配金とか。なのでブロックチェーンという個別技術に拘り過ぎずにサービスとしてゴールが実現できると良いですね。あと投機筋の過熱をどう抑制するか。SSI(自己主権型ID)とかもブロックチェーンはむしろ不向きに見えるしなぁ。このあたり興味のある方は夏休みの自由研究としていろいろ遊んでみるのも良いかもしれませんね。

それではHappy Hacking!

参考

https://paiza.hatenablog.com/entry/2018/05/11/Pythonでブロックチェーンを実装して採掘までやって

Discussion