はじめに
最近、Web3に関するツイートやニュースの中でも、Non-Fungible Token (NFT)を目にすることが多い。
NFTはブロックチェーンに基づいた技術で、その改ざん不可能性が肝となっているらしい。
以前からブロックチェーンの改ざん不可能性については知っていたものの、
- なぜ改ざんが不可能なのか
- 改ざんを試みると、どうなるのか
といった疑問が残っていた。
今回は、これらの疑問を解決すべく、自分でブロックチェーンを実装し、送金データの投入を行う。更に、送金データの改ざんを試みることで、さらなる理解を目指す。
ブロックチェーン
ブロックチェーンは、hash値、data、nonceで構成される「ブロック」がつながったものである。
データを読み取る時は、各ブロックが持っているdataを読み取るのみでよい。
競合する複数のブロックチェーンが存在した時、つながっているブロック数が最大のもののみを信用する。
hash値
なんらかのデータをhash化することによって得られた値。
h = H(d)
ここで$h$,$H$,$d$はそれぞれhash値、hash関数、データである。hash関数に期待される性質として、
- $H(d)$の計算が簡単
- $h$から$d$の推定が困難
の2つが挙げられる。ビットコインで用いられているハッシュ関数「SHA256」の入出力は下記サイトで確認できるため、興味があれば遊んでみてほしい。
https://www.milk-island.net/javascript/hashgenerator/sha2_256.html
またpythonにおいては、下記コードで辞書data
のhash値を得られる。
import hashlib
import json
print(hashlib.sha256(json.dumps(data).encode()).hexdigest())
data
今回は主に送金データを扱う。そのため、次のような辞書をブロックに持たせる。
data = {
'from': 'Aさん',
'to': 'Bさん',
'amount': 7000
}
このデータを持ったブロックが存在すると、AさんからBさんへ7000の送金があったことを意味する。
nonce
ブロックの有効性の検証に使用される値である。
ブロックチェーンでは、「あるブロック$B$が有効である」ことと、「そのブロック自身のhash値$H(B)$がある条件$C$を満たすこと」とを等価とする。よく用いられる$C$には、「$H(B)$を2進数で表記したときに、最初の$D$桁が0であること」がある。またこの$D$を採掘難易度と呼ぶ。$H(B)$は$B$に依存し、$B$はnonceに依存するため、$H(B)$はnonceに依存する。
ブロックが「つながる」とは
ブロック$B_n$と$B_{n-1}$が存在し、$B_n$の持つhash値が$B_{n-1}$のhash値$H(B_{n-1})$と等しいとき、$B_n$は$B_{n-1}$の後ろにつながっていると言う。
実装
上記を踏まえ、ブロック、ブロックチェーン、ブロックチェーンを用いた送金データ投入をそれぞれ実装してゆく。
ブロック
Blockクラスを実装する。
コンストラクタで採掘難易度と前ブロックのhash値を設定し、set_data
でデータの投入を、get_data
でデータの取り出しを行う。なお、set_data
のwhile文の中で、そのブロック自身が有効となるようなnonceを探している。これは「マイニング」と呼ばれる作業である。
import hashlib
import json
import typing as t
class Block:
_diff: int
_data: t.Dict
_hash: str
_nonce: int
@staticmethod
def _get_hash(data: t.Dict) -> str:
return hashlib.sha256(json.dumps(data).encode()).hexdigest()
def __init__(self, difficulty: int, prev_hash: str = None) -> None:
self._diff = difficulty
self._nonce = 0
self._hash = '0' * 64 if prev_hash is None else prev_hash
def _is_valid_hash(self, hash_: str) -> bool:
bin_ = str(bin(int(hash_, 16)))[2:].zfill(256)
return bin_.startswith('0' * self._diff)
def set_data(self, data: t.Dict) -> None:
while True:
hash_ = self._get_hash({'data': data, 'nonce': self._nonce, 'hash': self._hash})
if self._is_valid_hash(hash_):
self._data = data
break
else:
self._nonce += 1
def get_data(self) -> t.Dict:
return self._data
def get_hash(self) -> str:
return self._hash
def get_next_hash(self) -> str:
return self._get_hash({'data': self._data, 'nonce': self._nonce, 'hash': self._hash})
def get_is_valid_block(self) -> bool:
return self._is_valid_hash(self._get_hash({'data': self._data, 'nonce': self._nonce, 'hash': self._hash}))
ブロックチェーン
ブロックチェーンの全ブロックから、各ユーザの所持金を簡単に取得するために、BlockChainクラスを実装する。
from block import Block
import typing as t
class BlockChain:
def __init__(self):
self.blocks: t.List[Block] = []
def append(self, block: Block):
self.blocks.append(block)
def _get_all_data(self) -> t.List[t.Dict]:
out: t.List[t.Dict] = []
for b in self.blocks:
out.append(b.get_data())
return out
def get_balances(self) -> t.Dict:
balances: t.Dict = dict()
people_names: t.List[str] = []
blocks = self._get_all_data()
for b in blocks:
from_ = b['from']
to = b['to']
if from_ not in people_names:
people_names.append(from_)
if to not in people_names:
people_names.append(to)
for name in people_names:
balances[name] = 0
for b in blocks:
from_ = b['from']
to = b['to']
amount = b['amount']
balances[from_] -= amount
balances[to] += amount
return balances
def is_valid_chain(self) -> bool:
prev_hash = '0' * 64
for b in self.blocks:
if not b.get_is_valid_block():
print('INVALID BLOCK!')
return False
if prev_hash != b.get_hash():
print('INVALID CONNECTION!')
return False
prev_hash = b.get_next_hash()
return True
メイン部
上で実装したブロックチェーンに、実際に送金データを投入してみる。辞書data_list
が示すように、AさんからBさんへの100の送金、BさんからCさんへの50の送金、AさんからDさんへの50の送金の3つのデータを投入する。送金前は全ユーザの所持金を0としているため、BlockChain#get_balances()
により示される各ユーザの所持金が
Aさん: -150
Bさん: 50
Cさん: 50
Dさん: 50
となれば正しく投入できていることが確認できる。
from block import Block
from block_chain import BlockChain
def main():
data_list = [
{
'from': 'Aさん',
'to': 'Bさん',
'amount': 100
},
{
'from': 'Bさん',
'to': 'Cさん',
'amount': 50
},
{
'from': 'Aさん',
'to': 'Dさん',
'amount': 50
},
]
difficulty = 10
prev_hash = None
block_chain = BlockChain()
for data in data_list:
block = Block(difficulty=difficulty, prev_hash=prev_hash)
print('mining...')
block.set_data(data)
print('done.')
prev_hash = block.get_next_hash()
block_chain.append(block)
print('balances:', block_chain.get_balances())
print('valid:', block_chain.is_valid_chain())
if __name__ == '__main__':
main()
実行結果
mining...
done.
mining...
done.
balances: {'Aさん': -150, 'Bさん': 50, 'Cさん': 50, 'Dさん': 50}
valid: True
期待通りの動作が確認できた。
改ざんを試みる
データをブロックチェーンに保存することで得られる最大の恩恵は、改ざんが不可能になることであろう。実際に改ざんを試みると、どんなことが起こるのだろうか。
改ざんしやすいブロックを実装
Blockクラスを継承し、dataのみのセット、nonceのみのセットのためのインスタンスメソッドをそれぞれ実装した。
from block import Block
import typing as t
class VulnerableBlock(Block):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def set_data_without_mining(self, data: t.Dict) -> None:
self._data = data
def set_nonce(self, nonce: int) -> None:
self._nonce = nonce
data_list[1]
は、BさんからCさんへ50の送金があったことを示すが、Aさんからハッカーへ10000000の送金があったことを示すデータへの改ざんを試みる。
from vulnerable_block import VulnerableBlock
from block_chain import BlockChain
def main():
data_list = [
{
'from': 'Aさん',
'to': 'Bさん',
'amount': 100
},
{
'from': 'Bさん',
'to': 'Cさん',
'amount': 50
},
{
'from': 'Aさん',
'to': 'Dさん',
'amount': 50
},
]
difficulty = 10
prev_hash = None
block_chain = BlockChain()
for data in data_list:
block = VulnerableBlock(difficulty=difficulty, prev_hash=prev_hash)
print('mining...')
block.set_data(data)
print('done.')
prev_hash = block.get_next_hash()
block_chain.append(block)
print('balances:', block_chain.get_balances())
print('valid:', block_chain.is_valid_chain())
# ここまで、BlockがVulnerableBlockに変わった事以外は、main.pyと同じ。
# hacked_blockに、Aさんからハッカーへ10000000の送金があったデータを投入する。
print('改ざんスタート')
hacked_block = VulnerableBlock(difficulty=difficulty)
# BさんからCさんへの50の送金のかわりに、Aさんからハッカーへ10000000の送金があったことにする。
hacked_block.set_data({
'from': 'Aさん',
'to': 'ハッカー',
'amount': 10000000
})
block_chain.blocks[1] = hacked_block
print('balances:', block_chain.get_balances())
print('valid:', block_chain.is_valid_chain())
if __name__ == '__main__':
main()
実行結果
mining...
done.
mining...
done.
mining...
done.
balances: {'Aさん': -150, 'Bさん': 50, 'Cさん': 50, 'Dさん': 50}
valid: True
改ざんスタート
balances: {'Aさん': -10000150, 'Bさん': 100, 'ハッカー': 10000000, 'Dさん': 50}
INVALID CONNECTION!
valid: False
INVALID CONNECTION!
1番目のブロックを書き換えたことで、そのhash値も変わり、2番目のブロックとのつながりが解除されてしまった。改ざん後のブロックチェーンの長さは2、改ざん前の長さが3であるため、改ざん前のブロックチェーンの方が信用される。つまり、改ざんに失敗したと言える。
まとめ
今回はブロックチェーンを実装し、実際にデータを投入した。これにより、近年の強力な技術の基礎を理解することができた。また、実際に改ざんを試み、失敗したことで、ブロックチェーンの長さに基づく信頼度の堅牢性も確認することができた。
参考にしたサイト
- Aicia Solid Project, 【あなただけに…】ブロックチェーンの仕組みだよ!【教えます…】, https://www.youtube.com/watch?v=bclE3gLeyzg
- 秋山, Pythonでブロックチェーンを実装して採掘までやってみたので解説する, https://paiza.hatenablog.com/entry/2018/05/11/Python%E3%81%A7%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%83%81%E3%82%A7%E3%83%BC%E3%83%B3%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%A6%E6%8E%A1%E6%8E%98%E3%81%BE%E3%81%A7%E3%82%84%E3%81%A3%E3%81%A6