Pythonでプライベートブロックチェーンネットワークを作ってみた

この記事は[NTTコムウェア AdventCalendar 2023](https://qiita.com/advent-calendar/2023/nttcomware) 13日目の記事です。

# はじめに
NTTコムウェアの管です！
最新技術に熱心で、システム設計から開発・運用までの経験を持つ、６年目のアプリ開発エンジニアです。現在、Web3.0技術検証ワーキンググループのリーダーを務めています。

# 記事向け対象
本記事は、ブロックチェーンに興味を持つエンジニアや、ブロックチェーンの勉強中の方を対象としています。

# きっかけ
- エンドユーザーAからエンドユーザーBにコインを送ることを検証していきますが、ブロックチェーンネットが必要のため、今回ご紹介させていただきます。
![実現したいこと.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/665dd3f3-176c-4d27-aab7-a5a8fb614239.png)

# 構築
## ブロックチェーンネットワーク構築
:::note info
Web3.0業界は非中央集権の特性から、ブロックチェーンネットワークが生まれました。
以下のイメージの赤枠を構築する方法について説明します。
:::
### イメージ
![ブロックチェーンネットワーク.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/fe24f57f-5f26-3904-fba4-9e28076350e4.png)
説明しやすいように、赤枠の詳細を以下に記載します。
![ブロックチェーンネットワーク (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/43c1b6a6-d140-b8f3-2232-0eb1ca226d86.png)
また、3つのDockerコンテナを起動するために、各コンテナは以下のように設定されています。

### 基本ネットワーク設定（プライベートネット場合）
| 項目 | blockchain_server5000 | blockchain_server5001 | blockchain_server5002 |
|:-:|:-:|:-:|:-:|
| Subnet | 172.16.0.0/16 | 172.16.0.0/16 | 172.16.0.0/16 |
| IPv4Address | 172.16.0.2 | 172.16.0.3 | 172.16.0.4 |
| Port | 5000 | 5001 | 5002 |
> 同じネットワーク内に設定しています。

一括起動しやすいため、以下のdocker-composeファイルを作りました。

<details><summary>docker-compose.yml</summary>

```yml
version: '3.8'

services:
  blockchain_server1:
    build:
      context: .
      dockerfile: Dockerfile
    image: blockchain_server
    container_name: blockchain_server5000
    ports:
      - 5000:5000
    volumes:
      - ./:/app
    command: python blockchain_server.py
    networks:
      blockchain_network:
        ipv4_address: 172.16.0.2

  blockchain_server2:
    image: blockchain_server
    container_name: blockchain_server5001
    ports:
      - 5001:5001
    volumes:
      - ./:/app
    command: python blockchain_server_5001.py
    networks:
      blockchain_network:
        ipv4_address: 172.16.0.3

  blockchain_server3:
    image: blockchain_server
    container_name: blockchain_server5002
    ports:
      - 5002:5002
    volumes:
      - ./:/app
    command: python blockchain_server_5002.py
    networks:
      blockchain_network:
        ipv4_address: 172.16.0.4

networks:
  blockchain_network:
    ipam:
      driver: default
      config:
        - subnet: 172.16.0.0/16

```

</details>

上記のファイルにより、一般的なネットワークを構築することができました。ブロックチェーンネットワークにおいては、各ノードが自動的に情報を取得する機能が必要です。次に、トランザクションの同期、検証、マイニング、およびブロックチェーンの更新といった動作を実現するために、Pythonでノード同士を見つける仕組みを作成します。

### 同士ノード判別

同士ノード判別と言っても、重要なのは疎通確認とIPアドレス操作です。

#### 疎通確認仕組み
今回はPyhon言語で<font color="Green">[socket](https://docs.python.org/ja/3/library/socket.html)</font>パッケージを使用して、順序性と信頼性のある双方向のバイトストリームを実現し、TCPソケット通信を利用したいです。そのため、ソケットタイプを<font color="Orange">SOCK_STREAM</font>に設定します。また、各ノードはIPv4を使うため、アドレスファミリーを<font color="Orange">AF_INET</font>に設定します。

```python
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
```
また、同士の状態を常に確認する必要があるのですが、今回はWith文を使ってみました

```python
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
```

:::note info
プログラム内でファイル操作や通信などを行う場合は、開始時の前処理と終了時の後処理を実行する必要があります。Pythonでは、このような場合にwith文を使用すると、前処理を実行し、ブロック内のメインの処理が終了した後に自動で後処理を実行してくれます。
:::

<details><summary>疎通確認の仕組みソースコード</summary>

```python
def is_found_host(target, port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(1)
        try:
            sock.connect((target, port))
            return True
        except Exception as ex:
            logger.error({
                'action': 'is_found_host',
                'target': target,
                'port': port,
                'ex': ex
            })
            return False
```
</details>

#### IP検索仕組み

基本的には、ノードのIPアドレス範囲を把握しています。そのため、For文を使用して検索機能を作成しています。
また、サブネット数を割り当てるため、文字列の組み合わせを正規表現でシンプルにしました。

~~~python
RE_IP = re.compile('(?P<host_ip>^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.)(?P<last_ip>\\d{1,3}$)')
~~~

##### 正規表現検証

~~~bash
>>> import re
>>> RE_IP = re.compile('(?P<host_ip>^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.)(?P<last_ip>\\d{1,3}$)')
>>> m = RE_IP.search('172.16.0.4')
>>> m.group('host_ip')
'172.16.0.'
>>> m.group('last_ip')
'4'
>>>
~~~

いい感じですね。最後の文字を分離できました。

<details><summary>IP検索の仕組みソースコード</summary>

```python
def find_neighbors(my_host, my_port, start_ip_range, end_ip_range, start_port, end_port):
    address = f'{my_host}:{my_port}'
    m = RE_IP.search(my_host)
    if not m:
        return None

    host_ip = m.group('host_ip')
    last_ip = int(m.group('last_ip'))
    neighbours = []
    for guess_port in range(start_port, end_port):
        for ip_range in range(start_ip_range, end_ip_range):
            guess_host = f'{host_ip}{int(last_ip) + int(ip_range)}'
            guess_address = f'{guess_host}:{guess_port}'
            if is_found_host(guess_host, guess_port) and not guess_address == address:
                neighbours.append(guess_address)
    return neighbours
```
</details>

#### 同士IPアドレスリスト洗い出し仕組み
検証のために、ハードコーディングして作成しました。固定値は以下のように設定しました。
~~~python
blockchain_port_range = (5000, 5003)
nerghbours_ip_range = (0, 1)
~~~

以下の関数では、ノード同士のIPアドレス範囲からIPアドレスリストを作成します。

~~~python
def set_neighbours(self):
        nerghbours_list_1 = utils.find_neighbors(
            '172.16.0.2', self.port,
            nerghbours_ip_range[0], nerghbours_ip_range[1],
            blockchain_port_range[0], blockchain_port_range[1])
        nerghbours_list_2 = utils.find_neighbors(
            '172.16.0.3', self.port,
            nerghbours_ip_range[0], nerghbours_ip_range[1],
            blockchain_port_range[0], blockchain_port_range[1])
        nerghbours_list_3 = utils.find_neighbors(
            '172.16.0.4', self.port,
            nerghbours_ip_range[0], nerghbours_ip_range[1],
            blockchain_port_range[0], blockchain_port_range[1])
        self.nerghbours = set(nerghbours_list_1) | set(nerghbours_list_2) | set(nerghbours_list_3)
        logger.info({
            'action': 'set_neighbours',
            'nerghbours': self.nerghbours
        })
~~~

#### IP検索<font color="Orange">自動</font>実行仕組み
ブロックチェーンノード側では、定期的に自動実行される仕組みが多く存在します。<font color="Red">同時実行を防ぐ</font>ために、一般的にはパフォーマンス向上のための仕組みを導入します。
今回は、計算機科学史上最も古い同期プリミティブの一つである[Semaphore(セマフォ)](https://docs.python.org/ja/3.5/library/threading.html#semaphore-objects)を使っています。

~~~python
blockchain_neighbhours_sync_time = 20

# 一次プロセスに設定する
self.sync_nerghbour_semaphore = threading.Semaphore(1)
~~~

また、[contextlib.ExitStack](https://docs.python.org/ja/3/library/contextlib.html#contextlib.ExitStack)を使って、20s間隔に同士ノードを検索する関数を構築しました。

~~~python
def sync_neighbours(self):
        is_acquire = self.sync_nerghbour_semaphore.acquire(blocking=False)
        if is_acquire:
            with contextlib.ExitStack() as stack:
                stack.callback(self.sync_nerghbour_semaphore.release)
                self.set_neighbours()
                # 20s繰り返し実行する
                loop = threading.Timer(
                    blockchain_neighbhours_sync_time,
                    self.sync_neighbours)
                loop.start()
~~~

#### ブロックチェーン同期仕組み
ここで、工夫した取得できた同士ノードのアドレスを使います。

~~~python
for node in self.nerghbours:
            response = requests.get(f'http://{node}/chain')
            if response.status_code == 200:
                response_json = response.json()
                chain = response_json['chain']
                chain_length = len(chain)
                if chain_length > max_length and self.valid_chain(chain):
                    max_length = chain_length
                    new_chain = chain
~~~

上記のソースから気づいたかもしれませんが、同士ノード側のAPIを用意する必要があります。
以下は受信用のAPIです。

~~~python
@app.route('/chain', methods=['GET'])
def get_chain():
    block_chain = get_blockchain()
    response = {
        'chain': block_chain.chain
    }
    return jsonify(response), 200
~~~


<details><summary>ブロックチェーン同期の仕組みソースコード</summary>

~~~python
def resolve_conflicts(self):
        new_chain = None
        max_length = len(self.chain)
        for node in self.nerghbours:
            response = requests.get(f'http://{node}/chain')
            if response.status_code == 200:
                response_json = response.json()
                chain = response_json['chain']
                chain_length = len(chain)
                if chain_length > max_length and self.valid_chain(chain):
                    max_length = chain_length
                    new_chain = chain
        if new_chain:
            self.chain = new_chain
            logger.info({'action': 'resolve_conflicts', 'status': 'replace_chain'})
            return True
        logger.info({'action': 'resolve_conflicts', 'status': 'note_replace_chain'})
        return False
~~~

</details>

# 結果検証

## コンテナ正常起動できる確認
~~~bash
$ docker-compose up -d 
Creating network "pythonblockchaincontainer_blockchain_network" with the default driver
Building blockchain_server1
[+] Building 2.9s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                 0.5s 
 => => transferring dockerfile: 32B                                                                                                  0.0s 
 => [internal] load .dockerignore                                                                                                    0.4s 
 => => transferring context: 2B                                                                                                      0.0s 
 => [internal] load metadata for docker.io/library/python:3.11-slim                                                                  2.4s 
 => [auth] library/python:pull token for registry-1.docker.io                                                                        0.0s 
 => [1/7] FROM docker.io/library/python:3.11-slim@sha256:cfd7ed5c11a88ce533d69a1da2fd932d647f9eb6791c5b4ddce081aedf7f7876            0.0s 
 => [internal] load build context                                                                                                    0.1s 
 => => transferring context: 14.41kB                                                                                                 0.0s 
 => CACHED [2/7] RUN apt-get update                                                                                                  0.0s 
 => CACHED [3/7] RUN apt-get install -y vim                                                                                          0.0s 
 => CACHED [4/7] WORKDIR /app                                                                                                        0.0s 
 => CACHED [5/7] COPY requirements.txt requirements.txt                                                                              0.0s 
 => CACHED [6/7] RUN pip3 install -r requirements.txt                                                                                0.0s 
 => [7/7] COPY . .                                                                                                                   0.1s 
 => exporting to image                                                                                                               0.2s 
 => => exporting layers                                                                                                              0.1s 
 => => writing image sha256:86aa38ce6e9a30869a759770750919e079587502354c369d6b0214c45ed466fc                                         0.0s 
 => => naming to docker.io/library/blockchain_server                                                                                 0.0s 
WARNING: Image for service blockchain_server1 was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating blockchain_server5002 ... done
Creating blockchain_server5000 ... done
Creating blockchain_server5001 ... done
~~~
いいですね、正常的に起動できました。またネット状況を確認しましょう。

~~~bash
$ docker network inspect pythonblockchaincontainer_blockchain_network
[
    {
        "Name": "pythonblockchaincontainer_blockchain_network",
        "Id": "c79cdcdf05e8818616d9ddf9f3bc481b476adf80e06b388da6082bd19751f1ed",
        "Created": "2023-12-06T14:41:51.198518578Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.16.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "701e05bbc823a16ec43cc1ed4e850a404daeea9212e1f0de0479263ba29a0739": {
                "Name": "blockchain_server5000",
                "EndpointID": "27963b22737e34be77c5dfa43d3130be46a53d5ad3ed6b1815619d28253322a6",
                "MacAddress": "02:42:ac:22:00:02",
                "IPv4Address": "172.16.0.2/16",
                "IPv6Address": ""
            },
            "cda9937497556fff008c24d04454ad641f9dc4535a5d17bf2e1baa88a9d0e357": {
                "Name": "blockchain_server5002",
                "EndpointID": "dfcca22e5b7b41008f370e246a77c25829742ff509df7ab54aedba6a556bfa85",
                "MacAddress": "02:42:ac:22:00:04",
                "IPv4Address": "172.16.0.4/16",
                "IPv6Address": ""
            },
            "e1dc3ac3de8a5a50cb55ff55e801e1709bbbb480dd08de4644cc2db13a998a66": {
                "Name": "blockchain_server5001",
                "EndpointID": "41c2e01efcf920d69fe1fff0e39ecad01fed5b4c40099d3b880fadce927e74ba",
                "MacAddress": "02:42:ac:22:00:03",
                "IPv4Address": "172.16.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "blockchain_network",
            "com.docker.compose.project": "pythonblockchaincontainer",
            "com.docker.compose.version": "1.29.2"
        }
    }
]
~~~

予測通りにネットワークを構築しています。

~~~bash
$ docker ps 
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS         PORTS                    NAMES
e1dc3ac3de8a   blockchain_server   "python blockchain_s…"   4 minutes ago   Up 4 minutes   0.0.0.0:5001->5001/tcp   blockchain_server5001
cda993749755   blockchain_server   "python blockchain_s…"   4 minutes ago   Up 4 minutes   0.0.0.0:5002->5002/tcp   blockchain_server5002
701e05bbc823   blockchain_server   "python blockchain_s…"   4 minutes ago   Up 4 minutes   0.0.0.0:5000->5000/tcp   blockchain_server5000

~~~

## 同士ノード検索の動作確認

### blockchain_server5000

~~~bash
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
INFO:blockchain:{'action': 'set_neighbours', 'nerghbours': {'172.16.0.4:5002', '172.16.0.3:5001'}}
~~~

### blockchain_server5001

~~~bash
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
INFO:blockchain:{'action': 'set_neighbours', 'nerghbours': {'172.16.0.2:5000', '172.16.0.4:5002'}}
~~~

### blockchain_server5002

~~~bash
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.2', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.3', 'port': 5002, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5000, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
ERROR:utils:{'action': 'is_found_host', 'target': '172.16.0.4', 'port': 5001, 'ex': ConnectionRefusedError(111, 'Connection refused')}    
INFO:blockchain:{'action': 'set_neighbours', 'nerghbours': {'172.16.0.3:5001', '172.16.0.2:5000'}}
~~~

各ノードから同士の存在を確認できた～～～

## ブロックチェーン同期の動作確認

APIから確認していきます。
![検証結果1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/d0138ebb-8bd3-2207-aa8e-1c499f6a6337.png)
三つのノードは同じチェーンと接続していることを確認できました。

![検証結果2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/dcd034e2-aa7b-99ef-7b8e-fc98ce42657c.png)
blockchain_server5000 ノード にMiningして、新しいブロックを追加したことを確認できました。

![検証結果3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/0ff3512f-aa96-449f-4090-5bac830ef956.png)
また、blockchain_server5001 ノード と　blockchain_server5002 ノードのチェーンを同期されたことを確認できました。
これで、ブロックチェーンネットワーク構築が完了しました。

# まとめ

本記事では、ブロックチェーンに興味を持つエンジニアや学習中の方を対象に、Web3.0業界の非中央集権性から生まれたブロックチェーンネットワークの構築方法について詳しく説明しました。

具体的には、赤枠で示されたイメージを構築するために、ノード同士の疎通確認やIPアドレス操作、TCPソケット通信などの手法が紹介しました。また、Dockerコンテナの設定やソケットタイプの選択など、具体的な実装方法も解説しました。

さらに、ノード同士のIPアドレス範囲からIPアドレスリストを作成し、同士ノードの機能を実現する方法も詳細に説明しました。また、ブロックチェーンノードの定期実行や同時実行を防ぐための仕組みも紹介しました。

以上

※本稿に記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。