LoginSignup
9
4

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

Last updated at Posted at 2023-12-12

この記事はNTTコムウェア AdventCalendar 2023 13日目の記事です。

はじめに

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

記事向け対象

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

きっかけ

  • エンドユーザーAからエンドユーザーBにコインを送ることを検証していきますが、ブロックチェーンネットが必要のため、今回ご紹介させていただきます。
    実現したいこと.png

構築

ブロックチェーンネットワーク構築

Web3.0業界は非中央集権の特性から、ブロックチェーンネットワークが生まれました。
以下のイメージの赤枠を構築する方法について説明します。

イメージ

ブロックチェーンネットワーク.png
説明しやすいように、赤枠の詳細を以下に記載します。
ブロックチェーンネットワーク (1).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ファイルを作りました。

docker-compose.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

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

同士ノード判別

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

疎通確認仕組み

今回はPyhon言語でsocketパッケージを使用して、順序性と信頼性のある双方向のバイトストリームを実現し、TCPソケット通信を利用したいです。そのため、ソケットタイプをSOCK_STREAMに設定します。また、各ノードはIPv4を使うため、アドレスファミリーをAF_INETに設定します。

socket.socket(socket.AF_INET, socket.SOCK_STREAM)

また、同士の状態を常に確認する必要があるのですが、今回はWith文を使ってみました

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

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

疎通確認の仕組みソースコード
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

IP検索仕組み

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

RE_IP = re.compile('(?P<host_ip>^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.)(?P<last_ip>\\d{1,3}$)')
正規表現検証
>>> 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'
>>>

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

IP検索の仕組みソースコード
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

同士IPアドレスリスト洗い出し仕組み

検証のために、ハードコーディングして作成しました。固定値は以下のように設定しました。

blockchain_port_range = (5000, 5003)
nerghbours_ip_range = (0, 1)

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

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検索自動実行仕組み

ブロックチェーンノード側では、定期的に自動実行される仕組みが多く存在します。同時実行を防ぐために、一般的にはパフォーマンス向上のための仕組みを導入します。
今回は、計算機科学史上最も古い同期プリミティブの一つであるSemaphore(セマフォ)を使っています。

blockchain_neighbhours_sync_time = 20

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

また、contextlib.ExitStackを使って、20s間隔に同士ノードを検索する関数を構築しました。

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()

ブロックチェーン同期仕組み

ここで、工夫した取得できた同士ノードのアドレスを使います。

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です。

@app.route('/chain', methods=['GET'])
def get_chain():
    block_chain = get_blockchain()
    response = {
        'chain': block_chain.chain
    }
    return jsonify(response), 200
ブロックチェーン同期の仕組みソースコード
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

結果検証

コンテナ正常起動できる確認

$ 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

いいですね、正常的に起動できました。またネット状況を確認しましょう。

$ 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"
        }
    }
]

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

$ 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

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

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

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
三つのノードは同じチェーンと接続していることを確認できました。

検証結果2.png
blockchain_server5000 ノード にMiningして、新しいブロックを追加したことを確認できました。

検証結果3.png
また、blockchain_server5001 ノード と blockchain_server5002 ノードのチェーンを同期されたことを確認できました。
これで、ブロックチェーンネットワーク構築が完了しました。

まとめ

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

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

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

以上

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

9
4
2

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
9
4