0
2

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 1 year has passed since last update.

ブロックチェーンを実装してみた & 改ざんを試みた

Posted at

はじめに

最近、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を探している。これは「マイニング」と呼ばれる作業である。

block.py
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クラスを実装する。

block_chain.py
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
となれば正しく投入できていることが確認できる。

main.py
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のみのセットのためのインスタンスメソッドをそれぞれ実装した。

vulnerable_block.py
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の送金があったことを示すデータへの改ざんを試みる。

hack.py
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であるため、改ざん前のブロックチェーンの方が信用される。つまり、改ざんに失敗したと言える。

まとめ

今回はブロックチェーンを実装し、実際にデータを投入した。これにより、近年の強力な技術の基礎を理解することができた。また、実際に改ざんを試み、失敗したことで、ブロックチェーンの長さに基づく信頼度の堅牢性も確認することができた。

参考にしたサイト

0
2
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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?