SymbolPeerNodeとPythonで話する

長らく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健康さんのピアは高確率で空データを返します。
同期に時間かかる原因かな?

フォーマットのイメージはこんな感じ。

ヘッダーはリクエストした値そのまま入ってます。リクエストしたものと一致しているか確認しましょう。無事、期待したレスポンスであればデータ部分を読みます。
データ部分のフォーマットは以下。

項目長さ
version4バイト
publicKey32バイト
networkGenerationHashSeed32バイト
roles4バイト
port2バイト
networkIdentifier1バイト
host_length1バイト
friendly_name_length1バイト
hosthost_length
friendlyNamefriendly_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から取れる証明書から取得してね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

14 + fifteen =