15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nem / symbolAdvent Calendar 2023

Day 3

SymbolピアノードとPythonで会話する

Posted at

ピア「聞こえますか……?」

ピアがしゃべった!!

はじめに

というわけで、長らく Peer は無口というか未知の言語でノード間で会話していると思ってましたが、下記ジャガーさんの記事により会話に参加することが可能ということがわかりました。(あとで気付いたけどテクニカルホワイトペーパーにふんわり書いてあった

ピア間の通信って?

ピア間の通信は SSL ソケット通信で行われてます。ソケット通信は TCP/IP を利用する通信全般のことであるため、トランスポート層になります。HTTP などのアプリケーション層より下位の層になります。普段プログラム書くのもアプリケーション層部分を使用して書くので、通常ソケット通信なんて使用しません。この辺りが、取っつきにくさ感が出てくる所ですね。

ノード証明書の生成

ノード証明書というと、よく「ノードの証明書を更新してください。」などアナウンスがある、あの証明書です。ちなみに期限が切れると他のノードとの通信が確立しなくなり、孤立して独自のチェーンを紡ぎ始めます。つまりフォークします。

そんなわけで証明書がないと通信出来ないので、証明書を用意します。
symbol-node-configurator を使用すると簡単に証明書を作成することが出来ます。ただ、公式リポジトリにあるやつだと古い SymbolSDK を使用するので、新しい方の 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 でラップしてピアと仲良く握手。
以下は、symbol02.harvestasya.com:7900に接続したらすぐ切断するコードです。

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

リクエスト

無事、ピアと接続できたらリクエストを送信します。
ブロック高取得は、ジャガーさんがもう書いてるので今回はノード情報にしましょう。

送信するのはヘッダーのみで、ペイロードは必要ありません(というかペイロード付きはまだ出来てないです...)。
ヘッダーの内容は下記の 8 バイトです。

  • ヘッダーサイズ 4 バイト
  • パケットタイプ 4 バイト

パケットタイプはリクエストを判別するためのコードです。

他のパケットタイプについては、 PacketType.h を参照してください。

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

symbol_header.jpg

この 8 バイトを送信します。
以下は、symbol02.harvestasya.com:7900に接続してヘッダー作って送信するコードです。

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

レスポンスを解読

正常に処理されるとピアから返事が返ってきます。エラーになると空データが返ります。

レスポンスはヘッダー 8 バイト、ペイロードサイズ 4 バイト、ペイロード n バイトとなってます。
ヘッダーはリクエストした値そのまま入ってますので、リクエストしたものと一致しているか確認しましょう。無事、期待したレスポンスであればペイロード部分を読みます。
Node_Discovery_Pull_Ping のペイロード部分のフォーマットは以下です。

項目 長さ
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

以下は、symbol02.harvestasya.com:7900に接続してヘッダー作って送受信して出力するコードです。

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 から取れる証明書から取得してね。

さいごに

これでピアノードから情報を引き出すことが出来るようになりました。ノード運用する上で必要な情報は取れるので、ハーベスト目的であればデュアルノードを運用する必要もなくなりますので、VPSならスペックダウンでき費用も下げれます。

15
10
0

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
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?