この記事はNTTコムウェア AdventCalendar 2023 13日目の記事です。
はじめに
NTTコムウェアの管です!
最新技術に熱心で、システム設計から開発・運用までの経験を持つ、6年目のアプリ開発エンジニアです。現在、Web3.0技術検証ワーキンググループのリーダーを務めています。
記事向け対象
本記事は、ブロックチェーンに興味を持つエンジニアや、ブロックチェーンの勉強中の方を対象としています。
きっかけ
構築
ブロックチェーンネットワーク構築
Web3.0業界は非中央集権の特性から、ブロックチェーンネットワークが生まれました。
以下のイメージの赤枠を構築する方法について説明します。
イメージ
説明しやすいように、赤枠の詳細を以下に記載します。
また、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から確認していきます。
三つのノードは同じチェーンと接続していることを確認できました。
blockchain_server5000 ノード にMiningして、新しいブロックを追加したことを確認できました。
また、blockchain_server5001 ノード と blockchain_server5002 ノードのチェーンを同期されたことを確認できました。
これで、ブロックチェーンネットワーク構築が完了しました。
まとめ
本記事では、ブロックチェーンに興味を持つエンジニアや学習中の方を対象に、Web3.0業界の非中央集権性から生まれたブロックチェーンネットワークの構築方法について詳しく説明しました。
具体的には、赤枠で示されたイメージを構築するために、ノード同士の疎通確認やIPアドレス操作、TCPソケット通信などの手法が紹介しました。また、Dockerコンテナの設定やソケットタイプの選択など、具体的な実装方法も解説しました。
さらに、ノード同士のIPアドレス範囲からIPアドレスリストを作成し、同士ノードの機能を実現する方法も詳細に説明しました。また、ブロックチェーンノードの定期実行や同時実行を防ぐための仕組みも紹介しました。
以上
※本稿に記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。