25
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Goかpythonでブロックチェーンを理解する

Last updated at Posted at 2019-07-19

 最近ビットコインがまた上昇しているという話ですね。五月あたりに一気に上昇したとかで私のTwitterのTLにいるビットコインの取引している人はめちゃめちゃ喜んでました。そんなわけで今回はブロックチェーンがどのような仕組みになっているかGoとpython(Goとpythonで同じ内容を実装します。所属学部の関係上、pythonが読める人が友達に多いので)で実装しながら説明していきます。最後にまあ最低限ブロックチェーンのシミュレーションといっても差し支えないだろうというもの(主観です)をGoで実装します。

#ブロックチェーンとは何か

ブロックチェーン-Wikipedia

ブロックチェーンとは、分散型台帳技術、または、分散型ネットワークである。ビットコインの中核技術(サトシ・ナカモトが開発)を原型とするデータベースである。

とあるように新しい種類のデータベースです。(これは私の考えです。でもたぶんあってます。)よく知られている応用例としてbitcoinやethereumなんかがあります。基本的にbitcoinはお金の記録を保存するもの、ethereumはそれに加えてプログラムの保存をするもののように考えています。ただ、近年はbitcoinにもethereumと同じような機能が実装されてきているらしいです。ブロックチェーンと聞くと「インターネット上にあるお金」を想像する方もいらっしゃると思うんですけど、ブロックチェーンの技術自体はお金とは無関係です。しかしその防御力の高さからかお金の保存や取引などに利用されます。(嘘です。みんながお金をかけても掘りたくなるような実装が組み込まれてるんですけど、それは今回はやりません。)

#ブロックチェーンの構造
###ハッシュ
ブロックチェーンを理解するのに欠かせない知識の一つにハッシュというものがあります。

ハッシュ-Wikipedia

ハッシュ、ハッシュ値 - データから算出した小さな値。各データを区別・表現する目的に用いる。

>>certutil -hashfile hogehoge.txt SHA256

windowsのコマンドプロンプトでこのように入力するとファイルのハッシュ値を計算することができます。ハッシュ値を算出するための関数をハッシュ関数といいます。ハッシュ関数は出力された値から入力された値を逆算することができないという特徴を持ちます。二つの異なるデータのハッシュ値は互に異なるように設計されているため(必ずしも違う値が出力されるわけではない)、データのハッシュ値が違う場合、互いのデータが違う値であるということが言えます。パスワードの保管などセキュリティが重視される場合、ハッシュ関数にはハッシュの衝突(別のデータのハッシュ値を計算したときに同じ値が出力される現象)ができるだけ少ないSHA(secure hash algorithmの略)と呼ばれる種類のハッシュ関数が使われます。

*ハッシュ値は5次関数に値を代入したらすぐに答えが求まるのに対して5次方程式は解けないというのとイメージは同じです。(たぶん)

##基本構造
ブロックチェーンの仕組み1.png

ブロックチェーンの基本構造は上の図のように(わかりにくかったらごめんなさい)あるブロックはその前のブロックのハッシュ値をもっているというリンクリストのような構造になっています。先頭は左のブロックです。ブロックチェーンの名前の由来はここです。これで何がうれしいかを説明します。まず、攻撃者が先頭のブロックのデータを書き換えたとします。そうすると次のブロックの中にある先頭のブロックのハッシュ値と、書き換えられた先頭のブロックのハッシュ値が一致しなくなります。そこで攻撃者は矛盾点を消すため二番目のブロックのハッシュ値を書き換えます。すると今度は二番目と三番目のブロックの間に矛盾が生じます。矛盾を消すために攻撃者が三番目のブロックを書き換えます。このように情報を書き換えようと攻撃した場合、書き換えたブロックから一番最後のブロックまですべてのブロックの情報を書き換えなければならないことになります。とりあえずここまでで攻撃するのはいっぱい書き換えなきゃいけないしなんか大変そうだなあってことがわかっていただければよいです。

##Goでプログラムを書いてみる

ここまでの説明をプログラムにしてみます。

まずはブロックの構造体とブロックを格納する配列(ブロックチェーン)を定義します。

//後で使うパッケージをimportしとく
package main

import
(
	"fmt"
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
)

//ブロックの構造体
type block struct {
	Prevhash [32]byte
}
//都合がいいのでハッシュ値を入れるとブロックを作ってくれる関数も作っとく
//ブロックを作成
func makeBlock(hash [32]byte) block{
	var a block
	a.Prevhash = hash
	return a
}

//ブロックチェーン
//最初のブロックのハッシュ値は0にしとく
var chain = []block{makeBlock([32]byte{})}

次にブロックをjson文字列にする関数と[]byteを渡すとSHA256のハッシュ値を計算してくれる関数を定義

//ブロックをjson文字列に変換する
func convjson(b block) string{
	var buf bytes.Buffer
	byt, _ := json.Marshal(b)
	buf.Write(byt)
	return buf.String()
}

//ハッシュ値を計算する関数
func hash(str []byte)  [32]byte {
	return [32]byte(sha256.Sum256(str))
}

最後にブロックをつなげてく関数をつくって終了!

//マイニング
func mine() string{
	lastBlock := chain[len(chain) - 1]
	lastBlock_json := convjson(lastBlock)
	n := makeBlock(hash([]byte(lastBlock_json)))
	chain = append(chain,n)
	return convjson(n)
}

//ハッシュ値はbyteの配列で保存されています。つまり1byteごとの数字の羅列
//かっちょよく16進数に変換したい場合はこの関数も定義しときましょう。
//[32]byteを[]byteに変換して16進数文字列に変換する
func convhex(b [32]byte) string{
	by := make([]byte,32)
	for i:=0;i<32;i++{
		by[i] = b[i]
	}
	return hex.EncodeToString(by)
}

ホリホリ

func main(){
	//マイニングの回数
	n := 5
	for i:=0;i<n;i++{
		mine()
	}
	fmt.Println(chain)
	/*
	さっきのconvhex関数を定義してたらこっちの方がカッコイイ
	for i:=0;i<len(chain);i++{
		fmt.Println(convhex(chain[i].Prevhash))
	}
	 */
}

##pythonでプログラムを書いてみる
pythonはGoよりもいくらか簡単にプログラムを書くことができます。すごく短いので説明はコメントアウトしてちょちょっと書きます。さっきと違うこととしては、ブロックを構造体ではなくdictionary(辞書)で作りました。

from hashlib import sha256 # ハッシュ関数
import json #dictionaryをjsonの文字列に変換するため

class blockChain():
    def __init__(self):
        self.chain = [{"prevhash":'0'*64}] # 初期値は0に
    #ディクショナリ、辞書、連想配列(いろんな呼び方があります。)
    #それでブロックを作る関数
    def makeBlock(self,hash):
        b = {} #新しいブロックをdictionaryで
        b["prevhash"] = hash
        return b
    #ブロックののjsonを取得しハッシュ値を計算する
    def hash(self,map):
        data = json.dumps(map) #dictionaryをjsonに変換
        #文字列をエンコードしてハッシュ値を計算して十六進数の文字列に変換
        hash = sha256(data.encode()).hexdigest()
        return hash
    #ブロックをつなげていく
    def mine(self):
        lastBlock = self.chain[-1] # 一番最後のブロックを取得
        hash = self.hash(lastBlock) # ハッシュ値を計算
        nextBlock = self.makeBlock(hash) # 新しいブロックを作る
        self.chain.append(nextBlock) # ブロックをつなげる

c = blockChain()
for i in range(5):
    c.mine()
print(c.chain)

こんな感じで書けます。

#ブロックチェーンをより安全にする
上では、ただただ前のブロックのハッシュ値を計算するだけでブロックをつなげていきました。しかし、ハッシュ値を計算するのは一瞬です。これだけの速度でチェーンを伸ばしていけると全て書き換えるのはかんたんです。なので、実際のブロックチェーンは一工夫されています。
2.png

上の部分はハッシュ値の計算からブロックの生成までのフローチャートです。前回の実装ではハッシュ値を計算したらそのままブロックを作成しています。しかし、工夫した実装ではハッシュ値に制限をもうけています。性能の良いハッシュ関数は出現する値の確率が一様です。ですので、制限を設けてあげるとそのハッシュ値が出るまで何回も何回も計算しなければいけません。ただし、同じデータについてハッシュ値を計算しても同じ値が出力されるだけです。ですので、ブロックに自由に変更できる要素を追加してその要素をごちゃごちゃ変えて条件を満たすハッシュ値を探していきます。
ナンス含.png
チェーンは上の図のようになります。これも先頭は左のブロックです。ハッシュ値は入力される値が少し変わるだけで大きく変動するのでごちゃごちゃ値をこねくり回して条件にあうものをさがします。ハッシュ値の制限を厳しくすればするほど合う値を探すのに時間がかかります。こうすることでブロックを改変するのに非常に大きなコストがかかって攻撃が受けにくくなります。

*ビットコインでは1ブロックつなげるのに平均10分程度かかるようにハッシュ値の制限がかかっています。また、時期によって多くの人が掘る場合やあまり掘られない場合があるので制限がちょうどいいくらいになるように2週間程度に一回制限が修正されます。

##Goでプログラムを書いてみる

【変更】まず、構造体にごちゃごちゃ値を変えてハッシュ値を探す値を追加します。この値は一般的にNonceと呼ばれるので変数名にそれを使います。ブロックチェーンの初期値にnonceを追加しときます。

//ブロックチェーン
var chain = []block{makeBlock([32]byte{},0)}

//ブロックチェーンの中身の構造体
type block struct {
	Prevhash [32]byte
	Nonce int //ごちゃごちゃ変える値
}

//ブロックを作成
func makeBlock(hash [32]byte,nonce int) block{
	var a block
	a.Prevhash = hash
	a.Nonce = nonce
	return a
}

【追加】閾値のバイト配列と二つのバイト配列を比較する関数を設定します。aに算出したハッシュ値、bに閾値を入れるとa<bのとき1が返されます。

//256bitのバイト列の比較 a < bのとき1
func comper(a [32]byte,b []byte) int{
	check := make([]byte,32)
	for i:=0;i<32;i++{
		check[i] = a[i]
	}
	return bytes.Compare(b,check)
}

閾値のバイト配列を生成します。指数の計算を今回は32bitで収まる範囲の整数の計算しかしないのでこんな感じで書いてますが、たぶんmathとかのパッケージにあると思うので使うといいと思います。


//指数
func pow(n int,m int) int{
	if m == 1{
		return n
	}else if m == 0 {
		return 1
	}else if m % 2 == 0{
		k := pow(n,m/2)
		return  k*k
	}else {
		return n*pow(n,m-1)
	}
}
//2^nの256bitのバイト列を生成
func makebyte(n int) []byte{
	ans := make([]byte,32)
	//256以上の値が引数に渡されたら制限がないといってよいので
	//とりあえず一番大きい桁に255入れときます。
	//(凝る人はすべてに255入れるループ書いても良いよ)
	if n >= 256{
		ans[0] = byte(255)
		return ans
	}
	amari := n % 8
	syo := n / 8
	check := pow(2,amari)
	ans[31-syo] = byte(check)
	return ans
}

【変更】mine関数のなかで、望んだ範囲のハッシュ値が算出されるまで何度もnonceを変更しながら計算するようにコードを変更します。

//マイニング diffが小さいほど制約が大きい
func mine(diff int) string{
	lastBlock := chain[len(chain) - 1]
	lastBlock_json := convjson(lastBlock)
	lastBlock_hash := hash([]byte(lastBlock_json))
	counter := 0
	limit := makebyte(diff)
	var n block
	for {
		n = makeBlock(lastBlock_hash,counter)
		nowBlock_json := convjson(n)
		nowBlock_hash := hash([]byte(nowBlock_json))
		if comper(nowBlock_hash,limit) == 1{
			break
		}
		counter ++
	}
	chain = append(chain,n)
	return convjson(n)
}

動かしてみます。

func main(){
	//マイニングの回数
	n := 5
	for i:=0;i<n;i++{
		block := mine(240) //難易度を240にしてホリホリ
		fmt.Println(string(block))
	}
}

出力
{"Prevhash":[205,238,133,119,21,139,235,115,177,165,36,115,169,188,168,141,124,99,113,3,149,95,2,232,207,114,60,12,88,69,28,28],"Nonce":280}
{"Prevhash":[0,0,6,23,177,48,240,93,239,0,196,205,228,167,78,52,50,128,27,144,125,8,172,233,27,33,169,127,40,252,55,220],"Nonce":18792}
{"Prevhash":[0,0,93,198,62,162,247,57,110,183,62,209,236,28,195,179,116,88,163,84,1,80,249,49,38,43,253,165,176,14,1,124],"Nonce":7932}
{"Prevhash":[0,0,232,216,132,117,74,174,19,142,116,9,172,50,160,160,64,209,74,47,215,0,182,22,132,178,3,178,166,254,253,70],"Nonce":62563}
{"Prevhash":[0,0,57,142,21,48,208,108,153,110,201,129,204,153,207,154,101,222,46,77,167,82,189,90,50,115,19,20,226,196,53,90],"Nonce":95247}

難易度をかえて掘るのに時間がかかることを確認してみてください。

##pythonでプログラムを書いてみる

【変更】さっきのmakeBlock関数にnonce値を加えます。nonce値はごちゃごちゃ変えて欲しいハッシュ値を探す値です。

def makeBlock(self,hash,nonce):
    b = {}
    b["prevhash"] = hash
    b["nonce"] = nonce
    return b

【変更】mine関数の引数にdiffという引数を追加します。これは、ハッシュ値の閾値です。ハッシュ値が2^diffよりも小さい値だった場合はブロックをつなげます。違う場合はnonceを変えてハッシュ値を計算しなおします。

def mine(self,diff):
    lastBlock = self.chain[-1]
    hash = self.hash(lastBlock)
    counter = 0
    while True:
        nextBlock = self.makeBlock(hash,counter)
        if int(self.hash(nextBlock),16) < 2**diff:break
        counter += 1
    self.chain.append(nextBlock)
    return nextBlock

実行!

c = blockChain()
n = 5
for i in range(n):
    block = c.mine(240)
    print(block)

出力
{'prevhash': 'fb197130829d450616b62e431f55fb639aa7ba5989dc82282aeb7b3435856677', 'nonce': 76517}
{'prevhash': '0000c25cb0503255c52ad386cde40b418ec7e0b876dde7cb40039195621c584d', 'nonce': 60994}
{'prevhash': '0000c4180e4cdc8bfaf24ec8738af39800bd08ef9926ab47a56a203194408276', 'nonce': 60232}
{'prevhash': '000030a575effc930f2c4d8529b39ad0e3052853e01b9d3e277dce3cd560877f', 'nonce': 240649}
{'prevhash': '000018c5d3e7bab0564d1b8dce874ed1efd0722c79427709a7ea304537b6cae0', 'nonce': 102298}

#ブロックチェーンが安全である理由

ここまでで、新しいブロックを生成するには非常に大きなコストがかかることがわかりました。しかし、これだけでは時間をかけて計算すればすべてのブロックを改変してしまうことができてしまうと思うと思います。ここで、Aさん、Bさん、Cさん、Dさんの四人がSumiSumicoinという暗号通貨をマイニングするときを考えます。四人が掘り始めていくらか時間が経ちました。その時に四人それぞれが掘り進めたブロックのチェーンが下のようになっているとします。
コンセンサス.png
このとき、全員のブロックが違ってはいけないので全員のブロックを統一します。結論から言いますと、統一するために一番長くチェーンをつなげた人のブロックを採用します。今回の場合でいくとBさんのブロックが採用されて、Aさん、Cさん、DさんはBさんのつなげたブロックをコピーします。また、ブロックのつながりに矛盾が生じるもの(ハッシュ値が一致しないもの)は採用しないようにします。このようにすることで、攻撃者がブロックの改ざんをする場合にはほかのまじめにマイニングしている人全員よりも速い速度でブロックをつなげる必要が出てきます。つまり、攻撃者はすべてのマイニングをする人の総和よりも多くの計算リソースを用意する必要が出てくるためそれはほぼ不可能です。私はそれだけ多くのコンピューターを用意して攻撃して採算が取れるとは思いません。まともな人間ならまずやらないでしょう。(個人的な意見です)

この後は複数人でマイニングをした場合を一つのパソコンでシミレーションをするということを行います。ですが、なかなかに長いコードになってしまい、型のないpythonで書くのは辛くなってしまったのでGoのみの実装となります。ご了承ください。

##Goで実装してみる

上にも書いたように複数人でマイニングをする場合を実装します。並列処理を実装することであたかも複数人でマイニングをしているように見せかけることができます。

とりあえずこんだけのパッケージをimportしとくと良いです。

import
(
	"math/rand"
	"fmt"
	"reflect"
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"time"
	"os"
)

まずはブロックの構造を変更します。複数人で掘るということで、マイナーの名前をブロックに記録することにします。また、他人のチェーンが整合性が取れているかどうか判別するためにハッシュ値の制限の閾値を入れてあげます。その変更に合わせてブロックを作成する関数も変更します。

//ブロックチェーンの中身の構造体
type block struct {
	Prevhash [32]byte
	Nonce int
	Difficulty int //難易度
	Name string //マイナーの名前
}

//ブロックを作成
func makeBlock(hash [32]byte,nonce int,difficulty int,name string) block{
	var a block
	a.Prevhash = hash
	a.Nonce = nonce
	a.Difficulty = difficulty
	a.Name = name
	return a
}

今回はブロックチェーンを複数人で掘ります。ですので、人数分のチェーンが必要になります。これに合わせてチェーンを初期化する関数も用意します。

//ブロックチェーン
//var chain = []block{makeBlock([32]byte{},0)}
//チェーンのリスト
var list = make([][]block,4)
//初期化
func initList(){
	for i:=0;i<4;i++{
		list[i] = []block{makeBlock([32]byte{},0,256,"")}
	}
}

チェーンの整合性が取れているかどうかを判別します。これは、他人が悪意のあるチェーンを作った時に自分のチェーンと間違えて置き換えないようにするためです。

//チェーンの整合性をチェック
func check(ls []block) bool{
	//前後のブロックとハッシュ値の関係に矛盾がないかどうか
	for i:=0;i<len(ls) - 1;i++{
		//前のブロックのハッシュ値
		before := hash([]byte(convjson(ls[i])))
		//次のブロックに書いてある前のブロックのハッシュ値
		after := ls[i+1].Prevhash
		if ! reflect.DeepEqual(after,before){
			return false
		}
	}
	//ハッシュ値が2^difficultyよりも小さいかどうか
	for i:=0;i<len(ls);i++{
		//制限のバイト配列
		limit := makebyte(ls[i].Difficulty)
		//現在のブロックのハッシュ値
		nowBlock_hash := hash([]byte(convjson(ls[i])))
		if comper(nowBlock_hash,limit) != 1{
			fmt.Println("yei")
			return false
		}
	}
	return true
}

一番長くて整合性のあるチェーンを返す関数です。ポインタだけ渡されるみたいなことがあると怖いので、最後にコピー走らせてます。

//全てのチェーンを調べて一番長くて整合性のあるチェーンを返す
func consensys() []block{
	//チェーンの長さを入れる。整合性のないチェーンの場合は-1
	chainLength := make([]int,len(list))
	for i:=0;i<len(list);i++{
		if check(list[i]){
			chainLength[i] = len(list[i])
		} else {
			chainLength[i] = -1
		}
	}
	maxLength := 0
	maxIndex := 0
	for i:=0;i<len(chainLength);i++{
		if chainLength[i] > maxLength{
			maxLength = chainLength[i]
			maxIndex = i
		}
	}
	ansChain := make([]block,maxLength)
	for i:=0;i<maxLength;i++{
		ansChain[i] = list[maxIndex][i]
	}
	return ansChain
}

mine関数も変えます。まず、1から順に探していたnonce値を乱数にします。これは、複数の人間が全く同じようにnonce値を探していたらちょっと早く掘り始めた人の一人勝ちになってしまうからです。(たぶん)実験はしてません。たぶんそうです。たぶん...
また、ひとブロック掘るごとに一番長いチェーンを自分のチェーンに置き換えます。この実装だと長いチェーンが自分だとしても自分のチェーンをコピーして置き換えるという操作をしているので凝る人は高速化できると思います。引数のindexはさっき作ったチェーンのリストのインデックスを指定してください。

//マイニング diffが小さいほど制約が大きい indexはチェーンのリストの何番目か
func mine(diff int,index int,name string) string{
	lastBlock := list[index][len(list[index]) - 1]
	lastBlock_json := convjson(lastBlock)
	lastBlock_hash := hash([]byte(lastBlock_json))
	limit := makebyte(diff)
	var n block
	for {
		//                          乱数にする↓
		n = makeBlock(lastBlock_hash,rand.Int(),diff,name)
		nowBlock_json := convjson(n)
		nowBlock_hash := hash([]byte(nowBlock_json))
		if comper(nowBlock_hash,limit) == 1{
			break
		}
	}
	//一番長いチェーンを自分の所に置き換える
	list[index] = append(list[index],n)
	list[index] = consensys()
	return convjson(n)
}

最後にホリホリするためのmain関数を書きます。A、B、C、Dの四人でマイニングをします。それぞれmine関数に名前とインデックスを指定します。また、もう一つ5秒おきに最長のチェーンの状態を出力してくれるものも並列で実行しときます。

func main(){
	initList()
	//Aさん
	go func() {
		for {
			mine(235,0,"A")
		}
	}()
	//Bさん
	go func() {
		for {
			mine(235,1,"B")
		}
	}()
	//Cさん
	go func() {
		for {
			mine(235,2,"C")
		}
	}()
	//Dさん
	go func() {
		for {
			mine(235,3,"D")
		}
	}()

	//五秒おきに最長チェーンの状態を出力
	go func() {
		for {
			time.Sleep(5*time.Second)
			ls := consensys()
			for i:=0;i<len(ls);i++{
				fmt.Println(ls[i])
			}
		}
	}()
	//四人で5分マイニングした後最長チェーンの状態をファイルに書き込む
	time.Sleep(300*time.Second)
	//                     ファイル名を指定↓
	file, err := os.Create(`directory\\hogehoge.txt`)
	if err != nil {
		// Openエラー処理
	}
	defer file.Close()

	ls := consensys()
	for i:=0;i<len(ls);i++{
		output := convjson(ls[i]) + "\r\n"
		file.Write(([]byte)(output))
	}
}

ここまで出来たら実行してあげましょう。五分後には自作ブロックチェーンの結果がファイルに出力されているはずです。私のPC(2コア4スレッド2.50GHz - 2.71GHz)では235がちょうどよい難易度だったのでそれになってまずが、もっといいプロセッサが積んであるコンピューターではもう少し数字を下げてあげてもいいかもしれません。

私が出力したやつ

このソースコード

#最後に
ここまで作ってきましたが、やはりブロックチェーンの構造は面白いですね。これでとりあえず変更されにくいチェーンを組めたので、あとはブロックの中にどんな機能を入れるかによってさまざまな性質のブロックチェーンを作ることができます。世の中にたくさんの暗号通貨が存在しているのはそのためです。(ほかにさして知識のない人が自分の通貨を作りたいと言って作る人もいますが...)なんにせよ、ブロックチェーンは未来ある技術なので今後の展開が楽しみです。あと、私はこれからテスト期間に入るので勉強頑張ります。

25
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?