長らくPeerへ問い合わせは出来ないと思ってましたが、この記事により問い合わせ出来ることがわかりました。(あとで気付いたけどホワイトペーパーに書いてあった
さて、この記事を読んでPeerToolsを作ったわけですが、勢いで作ったので忘れる可能性が高いので、どんなことをやっているか残しておこうと思います。
ピアへの接続
ピア間の通信はノード証明書を使ったSSLソケット通信で行われてます。
ノード証明書というと、よく「ノードの証明書を更新してください。」などアナウンスがある、あの証明書です。ちなみに期限が切れると他のノードとの通信が確立しなくなり孤立し独自のチェーンを紡ぎ始めます。
という訳で、接続するための証明書を用意します。
symbol-node-configurator を使用すると簡単に証明書を作成することが出来ます。ただ、公式リポジトリにあるやつだと古いSymbolSDK(Python)を使用するので、新しい方のSymbolSDKに対応したのを置いてます(証明書作成するところだけ動くの確認済)。
Linuxの場合
$ pip install symbol-sdk-python zenlog --user
$ git clone https://github.com/ccHarvestasya/symbol-node-configurator.git
$ openssl genpkey -algorithm ed25519 -outform PEM -out ca.key.pem
$ python3 ../symbol-node-configurator/certtool.py --working cert --name-ca "my cool CA" --name-node "my cool node name" --ca ca.key.pem
$ cat cert/node.crt.pem cert/ca.crt.pem | sudo tee cert/node.full.crt.pem
Windowsの場合
OpenSSLはココからダウンロード。Win64かWin32は環境で選んでください。Lightで良いと思います。EXE、MSIはお好みで。
> pip install symbol-sdk-python zenlog --user
> git clone https://github.com/ccHarvestasya/symbol-node-configurator.git
> openssl genpkey -algorithm ed25519 -outform PEM -out ca.key.pem
> python ../symbol-node-configurator/certtool.py --working cert --name-ca "my cool CA" --name-node "my cool node name" --ca ca.key.pem
> cat cert/node.crt.pem > cert/node.full.crt.pem
> cat cert/ca.crt.pem >> cert/node.full.crt.pem
証明書の準備ができたら、ソケット通信をSSLでラップしてピアと仲良く握手。
import os
import ssl
import socket
import json
from pathlib import Path
from symbolchain.BufferReader import BufferReader
from symbolchain.BufferWriter import BufferWriter
NODE_HOST = "symbol02.harvestasya.com"
NODE_PORT = 7900
CERTIFICATE_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + "/cert"
certificate_directory = Path(CERTIFICATE_DIRECTORY)
timeout = 5
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
ssl_context.load_cert_chain(
certificate_directory / "node.full.crt.pem",
keyfile=certificate_directory / "node.key.pem",
)
try:
with socket.create_connection((NODE_HOST, NODE_PORT), timeout) as sock:
with ssl_context.wrap_socket(sock) as ssock:
print("接続しました")
except socket.timeout as ex:
raise ConnectionRefusedError from ex
接続したらすぐ切断するコード
リクエスト
無事、ピアと接続できたらリクエストを送信します。
ブロック高取得は、ジャガーさんがもう書いてるので今回はノード情報にしましょう。
送信するのはヘッダーのみで、ペイロードは必要ありません。ヘッダーの内容はヘッダーサイズ4バイト、リクエストを判別するためのパケットタイプ4バイトです。
パケットタイプについては、 PacketType.h を参照してください。
フォーマットイメージはこんな感じ。

ノード情報のパケットタイプ(Node_Discovery_Pull_Ping)は0x111です。リトルエンディアンで格納するので、イメージはこんな感じ。

この8バイトを送信します。
import os
import ssl
import socket
import json
from pathlib import Path
from symbolchain.BufferReader import BufferReader
from symbolchain.BufferWriter import BufferWriter
NODE_HOST = "symbol02.harvestasya.com"
NODE_PORT = 7900
CERTIFICATE_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + "/cert"
certificate_directory = Path(CERTIFICATE_DIRECTORY)
timeout = 5
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
ssl_context.load_cert_chain(
certificate_directory / "node.full.crt.pem",
keyfile=certificate_directory / "node.key.pem",
)
try:
with socket.create_connection((NODE_HOST, NODE_PORT), timeout) as sock:
with ssl_context.wrap_socket(sock) as ssock:
packet_type = 0x111
# ヘッダー作成
header_writer = BufferWriter()
header_writer.write_int(8, 4)
header_writer.write_int(packet_type , 4)
# ヘッダ送信
ssock.send(header_writer.buffer)
except socket.timeout as ex:
raise ConnectionRefusedError from ex
ヘッダー作って送信するコード
レスポンス
正常に処理されるとピアから返事が返ってきます。エラーになると空データが返ります。
ちなみにd健康さんのピアは高確率で空データを返します。
同期に時間かかる原因かな?
フォーマットのイメージはこんな感じ。

ヘッダーはリクエストした値そのまま入ってます。リクエストしたものと一致しているか確認しましょう。無事、期待したレスポンスであればデータ部分を読みます。
データ部分のフォーマットは以下。
項目 | 長さ |
version | 4バイト |
publicKey | 32バイト |
networkGenerationHashSeed | 32バイト |
roles | 4バイト |
port | 2バイト |
networkIdentifier | 1バイト |
host_length | 1バイト |
friendly_name_length | 1バイト |
host | host_length |
friendlyName | friendly_name_length |
import os
import ssl
import socket
import json
from pathlib import Path
from symbolchain.BufferReader import BufferReader
from symbolchain.BufferWriter import BufferWriter
class NodeDiscoveryPullPing:
def __init__(self):
self.version = 0
self.publicKey = ""
self.networkGenerationHashSeed = ""
self.roles = 0
self.port = 0
self.networkIdentifier = 0
self.host = ""
self.friendlyName = ""
def default_method(item):
if isinstance(item, object) and hasattr(item, "__dict__"):
return item.__dict__
else:
raise TypeError
NODE_HOST = "symbol02.harvestasya.com"
NODE_PORT = 7900
CERTIFICATE_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + "/cert"
certificate_directory = Path(CERTIFICATE_DIRECTORY)
timeout = 5
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
ssl_context.load_cert_chain(
certificate_directory / "node.full.crt.pem",
keyfile=certificate_directory / "node.key.pem",
)
try:
with socket.create_connection((NODE_HOST, NODE_PORT), timeout) as sock:
with ssl_context.wrap_socket(sock) as ssock:
packet_type = 0x111
# ヘッダー作成
header_writer = BufferWriter()
header_writer.write_int(8, 4)
header_writer.write_int(packet_type, 4)
# ヘッダ送信
ssock.send(header_writer.buffer)
# レスポンス空チェック
read_buffer = ssock.read()
if 0 == len(read_buffer):
raise ConnectionRefusedError(f"{NODE_HOST} から受信したデータが空っぽですよ!!")
# レスポンスデータ読み込み
size = BufferReader(read_buffer).read_int(4)
while len(read_buffer) < size:
read_buffer += ssock.read()
# レスポンスヘッダーチェック
reader = BufferReader(read_buffer)
size = reader.read_int(4)
actual_packet_type = reader.read_int(4)
if packet_type != actual_packet_type:
raise ConnectionRefusedError(
f"戻りのパケットタイプは、 {packet_type} を期待したけど {actual_packet_type} でした。ダメです!"
)
# ペイロードをノード情報クラスに詰め込み
node_discovery_pull_ping = NodeDiscoveryPullPing()
reader.offset = 12 # 先頭12バイト飛ばす(ヘッダー8バイト+ペイロードサイズ4バイト)
node_discovery_pull_ping.version = reader.read_int(4)
node_discovery_pull_ping.publicKey = reader.read_hex_string(32)
node_discovery_pull_ping.networkGenerationHashSeed = reader.read_hex_string(32)
node_discovery_pull_ping.roles = reader.read_int(4)
node_discovery_pull_ping.port = reader.read_int(2)
node_discovery_pull_ping.networkIdentifier = reader.read_int(1)
host_length = reader.read_int(1)
friendly_name_length = reader.read_int(1)
node_discovery_pull_ping.host = reader.read_string(host_length)
node_discovery_pull_ping.friendlyName = reader.read_string(friendly_name_length)
# ノード情報クラスをJsonで出力
print(json.dumps(node_discovery_pull_ping, default=default_method, indent=2))
except socket.timeout as ex:
raise ConnectionRefusedError from ex
データを受信して出力するコード
実行する
> python main.py
{
"version": 16777990,
"publicKey": "7587ECE8D3FA11A075E533E83F2F1CC8E09F7D2E1D1BD547A44AC5D4D4C78242",
"networkGenerationHashSeed": "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4",
"roles": 1,
"port": 7900,
"networkIdentifier": 152,
"host": "symbol02.harvestasya.com",
"friendlyName": "_Symbol_TestNet_HarvestasyaNode02/."
}
短い間隔で連続して実行すると空データを返します。
再実行は間を開けてやる。
えっ?nodePublicKeyがないって?
それはssockから取れる証明書から取得してね。
コメントを残す