事象
PythonがリンクするOpenSSLのバージョンによっては、ノードが正常に稼働しているにもかかわらず health コマンドが失敗する。
今回確認した環境では、Ubuntu 24標準のPythonはOpenSSL 3.0系とリンクしていた。
一方、uvで作成した仮想環境ではOpenSSL 3.5系とリンクしたPythonが入り、この事象が発生した。
PythonがリンクするOpenSSLのバージョンを調べるコマンドは以下。
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
再現
Python + OpenSSL3.0系
Ubuntu標準のPythonで仮想環境を作成
mkdir shoestring-openssl30
cd shoestring-openssl30
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install symbol-shoestring
バージョンの確認
$ python3 --version
Python 3.12.3
$ python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
OpenSSL 3.0.13 30 Jan 2024
shoestringのウィザードを使用して、Symbol Dualノードの環境を作成。
LC_MESSAGES=ja python3 -m shoestring.wizard
ノードを起動後、health コマンドを実行する。
docker compose up -d
LC_MESSAGES=ja python3 -m shoestring health --config shoestring/shoestring.ini --directory .
問題なくhealthコマンドが動作する。
$ LC_MESSAGES=ja python3 -m shoestring health --config shoestring/shoestring.ini --directory .
... | モジュール peer certificate のヘルスエージェントを実行
i | ca 証明書の有効期限にはまだ余裕があります (7299 日間)
i | node 証明書の有効期限にはまだ余裕があります (374 日間)
keys/cert/ca.crt.pem: OK
keys/cert/node.crt.pem: OK
... | モジュール peer API のヘルスエージェントを実行
i | ピアAPIにアクセス可能、ブロック高 = 1801
... | モジュール REST API のヘルスエージェントを実行
i | REST APIにアクセス可能、ブロック高 = 1801
... | モジュール REST websockets のヘルスエージェントを実行
i | エンドポイント ws://r.nemnesia.com:3000/ws にWebSocket接続中、ブロックを待機して購読中
i | WebSocketがブロックを受信しました。ブロック高 1802
Python + OpenSSL3.5系
uvでPython仮想環境を作成
mkdir shoestring-openssl35
cd shoestring-openssl35
uv venv --python 3.12
source .venv/bin/activate
uv pip install pip
pip install symbol-shoestring
バージョンの確認
$ python3 --version
Python 3.12.12
$ python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
OpenSSL 3.5.5 27 Jan 2026
shoestringのウィザードを使用して、Symbol Dualノードの環境を作成。
LC_MESSAGES=ja python3 -m shoestring.wizard
ノードを起動後、health コマンドを実行する。
docker compose up -d
LC_MESSAGES=ja python3 -m shoestring health --config shoestring/shoestring.ini --directory .
healthコマンドが失敗する。
$ LC_MESSAGES=ja python3 -m shoestring health --config shoestring/shoestring.ini --directory .
... | モジュール peer certificate のヘルスエージェントを実行
i | ca 証明書の有効期限にはまだ余裕があります (7299 日間)
i | node 証明書の有効期限にはまだ余裕があります (374 日間)
keys/cert/ca.crt.pem: OK
keys/cert/node.crt.pem: OK
... | モジュール peer API のヘルスエージェントを実行
[!!!] | ポート 7900 の localhost でピアAPIにアクセスできません
... | モジュール REST API のヘルスエージェントを実行
i | REST APIにアクセス可能、ブロック高 = 181
... | モジュール REST websockets のヘルスエージェントを実行
i | エンドポイント ws://r.nemnesia.com:3000/ws にWebSocket接続中、ブロックを待機して購読中
i | WebSocketがブロックを受信しました。ブロック高 200
原因
今回の不具合は、shoestring本体というより、内部で使用しているlightapiのSSL/TLSハンドシェイク処理にあると考えられる。
lightapiでは、ssl.SSLContext の内部表現から SSL_CTX * を固定オフセットで取り出し、SSL_CTX_set_verify を呼び出している。つまり、Pythonオブジェクトのメモリ配置に依存したABI前提の実装になっている。
# get python wrapper object address (SSL_CTX* is offset 16 bytes)
ssl_context_object_address = id(self.ssl_context)
ssl_context_raw_address = ctypes.cast(ssl_context_object_address, ctypes.POINTER(ctypes.c_uint64))[2]
ssl_context_pointer = ffi.cast('SSL_CTX *', ssl_context_raw_address)
self._verify_callback_wrapper = ffi.callback('int (*)(int, X509_STORE_CTX *)', self._verify_callback)
lib.SSL_CTX_set_verify(
ssl_context_pointer,
lib.SSL_VERIFY_PEER | lib.SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
self._verify_callback_wrapper)
この方法は、Python側の内部表現やリンク先のOpenSSLが変わると崩れる可能性がある。実際に今回の再現では、OpenSSL 3.0系とリンクしたPythonでは正常に動作し、OpenSSL 3.5系とリンクしたPythonではpeer APIの接続だけが失敗した。
そのため、原因は「OpenSSL 3.5そのものが悪い」というより、lightapiが前提にしているABIと、OpenSSL 3.5系をリンクしたPython実行環境のABIが一致せず、SSL_CTX_set_verify 周辺でハンドシェイクに失敗したことにある可能性が高い。
対応案
暫定対応(運用回避)
OpenSSL 3.0系とリンクしたPythonを使用する。
今回の再現環境では、Ubuntu 24標準Python(OpenSSL 3.0系)では health が成功したため、修正が入るまでの回避策としては有効。
ただし、この方法は実行環境をOpenSSL 3.0系に寄せる前提が必要であり、OpenSSL 3.5系を含む環境を同時に安定運用する解決にはならない。
恒久対応(推奨)
根本対応としては、lightapiのABI依存実装をやめる方向が望ましい。
具体的には次の2案がある。
- OpenSSL 3.5向けにABIを作り直す
- そもそもABIを使用しない
暫定対応では吸収しきれないため、実質的には恒久対応が必要になる。
保守性と再発防止の観点では、2の「ABIを使用しない」方針を推奨する。