見出し画像

【Symbol Blog】pythonの相互運用とvrf(2022/12/11)

この記事はSymbol Blogに投稿された記事「11 DEC PYTHON INTEROP AND VRF」を機械翻訳したものです。著者はSymbol/NEMのコア開発者であるJaguarさんです。


背景

このガイドでは、cffiを使用してpythonから基本的なCatapultクライアントC++関数を呼び出す手順を説明します。この例では、VRF.hから以下の関数を呼び出すことにします。

/// Generates a verifiable random function proof from \a alpha and \a keyPair.
VrfProof GenerateVrfProof(const RawBuffer& alpha, const KeyPair& keyPair);

/// Verifies verifiable random function proof (\a vrfProof) using \a alpha and \a publicKey.
Hash512 VerifyVrfProof(const VrfProof& vrfProof, const RawBuffer& alpha, const Key& publicKey);

/// Generates a verifiable random function proof hash from \a gamma.
Hash512 GenerateVrfProofHash(const ProofGamma& gamma);

あなたは、コードに沿って、またはここで完全に動作し、ステップバイステップで、コードを見つけることができます。

C API

CFFI は、C 関数のみをサポートしています。そのため、cffiを使用するためには、呼び出したいC++関数の周りに小さなCラッパーを作成する必要があります。

VRFPROOF

VrfProofはByteArrayベースの3つのフィールドを含む構造体である。ProofGamma と ProofScalar はそれぞれ 32 バイトで、ProofVerificationHash は 16 バイトである。

余談ですがByteArrayベースの型は、Catapultクライアントで型安全性を高めるために使用されます。ProofGammaとProofScalarは同じサイズ(32バイト)で、ByteArrayを専門としていますが、異なるタグを使用しているので、互換性がありません。

/// VRF proof gamma.
struct ProofGamma_tag { static constexpr size_t Size = 32; };
using ProofGamma = utils::ByteArray<ProofGamma_tag>;

/// VRF proof verification hash.
struct ProofVerificationHash_tag { static constexpr size_t Size = 16; };
using ProofVerificationHash = utils::ByteArray<ProofVerificationHash_tag>;

/// VRF proof scalar.
struct ProofScalar_tag { static constexpr size_t Size = 32; };
using ProofScalar = utils::ByteArray<ProofScalar_tag>;

/// VRF proof for the verifiable random function.
struct VrfProof {
	/// Gamma.
	ProofGamma Gamma;

	/// Verification hash.
	ProofVerificationHash VerificationHash;

	/// Scalar.
	ProofScalar Scalar;
};

動的型付け言語である Python で使用するために C のラッパーを作成するので、VrfProof フィールドの拡張型安全性を削除し、単純に固定サイズの配列を使用します。

struct CVrfProof {
	unsigned char Gamma[32];
	unsigned char VerificationHash[16];
	unsigned char Scalar[32];
};

CATAPULTGENERATEVRFPROOF

C++の宣言は

/// Generates a verifiable random function proof from \a alpha and \a keyPair.
VrfProof GenerateVrfProof(const RawBuffer& alpha, const KeyPair& keyPair);

alphaはRawBufferであり、これは2つのフィールドからなる可変サイズのバッファである。

/// Data pointer.
T* pData;

/// Data size.
size_t Size;

Cの宣言では、alphaを単純に、データを指すconst unsigned char*とデータのサイズを示すunsigned intの2つのパラメータに展開することができる。

keyPairはKeyPairのインスタンスであり、秘密鍵と公開鍵が一致することが保証されています。C言語でこの制約を強制するのは少し面倒です。

C言語宣言では、秘密鍵と公開鍵の両方を別々に渡すことができます。あるいは、秘密鍵だけを渡して、そこから公開鍵を導出することもできます。後者の場合、鍵の導出による追加的な性能コストが発生しますが、公開鍵と秘密鍵が常に一致することが保証されます。簡潔にするために、秘密鍵を指す固定サイズの const unsigned char* バッファを1つだけ使用することにします。

VrfProofは戻り値である。C言語では、呼び出し側が割り当ての決定を完全に制御できるように、大きな値をoutパラメータで返すのが一般的です。

したがって,C言語の宣言では,VrfProofをoutパラメータで返します.out パラメータの型は CVrfProof です.この関数は,戻り値を持ちません.

それをまとめると、Cの宣言は次のようになる。

PLUGIN_API
void CatapultGenerateVrfProof(
		const unsigned char* alpha,
		unsigned int alphaSize,
		const unsigned char* privateKey,
		struct CVrfProof* vrfProof);

C言語での実装は、かなり簡単です。

まず、C++の関数を呼び出せるように、Cの引数を用意する必要があります。

  1. alphaとalphaSizeでRawBufferを囲む

  2. privateKeyからKeyPairを作成し、公開鍵を導出する。

次に、C++の関数を呼び出す必要があります。

最後に、C++の結果をvrfProofのoutパラメータにコピーする必要があります。

全体として、実装は次のようになる。

using namespace catapult::crypto;

// 1. wrap KeyPair around private key
auto cppKeyPair = KeyPair::FromPrivate(PrivateKey::FromBuffer({ privateKey, PrivateKey::Size }));

// 2. call c++ function
auto cppVrfProof = GenerateVrfProof({ alpha, alphaSize }, cppKeyPair);

// 3. copy result
std::memcpy(vrfProof->Gamma, cppVrfProof.Gamma.data(), cppVrfProof.Gamma.size());
std::memcpy(vrfProof->VerificationHash, cppVrfProof.VerificationHash.data(), cppVrfProof.VerificationHash.size());
std::memcpy(vrfProof->Scalar, cppVrfProof.Scalar.data(), cppVrfProof.Scalar.size());

VERIFYVRFPROOF

C++の宣言は

/// Verifies verifiable random function proof (\a vrfProof) using \a alpha and \a publicKey.
Hash512 VerifyVrfProof(const VrfProof& vrfProof, const RawBuffer& alpha, const Key& publicKey);

vrfProof は,(C++)VrfProof のインスタンスであり, CVrfProof のパラメータに簡単に置き換えることができます.

alpha は、データを指す const unsigned char* と、データのサイズを示す unsigned int の 2 つのパラメータに展開される。

publicKey は、公開鍵を指す const unsigned char* 固定サイズバッファに置き換えられる。

戻り値は、outパラメータに置き換えられる。outパラメータは、結果の64バイトのハッシュを指す固定サイズのバッファ(const unsigned char*)である。

それをまとめると、Cの宣言は次のようになる。

PLUGIN_API
void CatapultVerifyVrfProof(
		const struct CVrfProof* vrfProof,
		const unsigned char* alpha,
		unsigned int alphaSize,
		const unsigned char* publicKey,
		unsigned char* hash512);

実装は上記と同じテンプレートに従い、以下のようになります。

using namespace catapult::crypto;
using PublicKey = catapult::Key;

// 1. create VrfProof from CVrfProof
VrfProof cppVrfProof;
std::memcpy(cppVrfProof.Gamma.data(), vrfProof->Gamma, ProofGamma::Size);
std::memcpy(cppVrfProof.VerificationHash.data(), vrfProof->VerificationHash, ProofVerificationHash::Size);
std::memcpy(cppVrfProof.Scalar.data(), vrfProof->Scalar, ProofScalar::Size);

// - copy publicKey to ByteArray
PublicKey cppPublicKey;
std::memcpy(cppPublicKey.data(), publicKey, PublicKey::Size);

// 2. call c++ function
auto cppHash512 = VerifyVrfProof(cppVrfProof, { alpha, alphaSize }, cppPublicKey);

// 3. copy result
std::memcpy(hash512, cppHash512.data(), cppHash512.size());

GENERATEVRFPROOFHASH

C++の宣言は

/// Generates a verifiable random function proof hash from \a gamma.
Hash512 GenerateVrfProofHash(const ProofGamma& gamma);

gamma は,証明ガンマを指す const unsigned char* 固定サイズバッファに置き換えられる。

戻り値は、outパラメータに置き換えられる。outパラメータは、結果の64バイトのハッシュを指す固定サイズのバッファ(const unsigned char*)である。

それをまとめると、Cの宣言は次のようになる。

PLUGIN_API
void CatapultGenerateVrfProofHash(const unsigned char* gamma, unsigned char* hash512);

実装は上記と同じテンプレートに従い、以下のようになります。

using namespace catapult::crypto;

// 1. copy gamma to ByteArray
ProofGamma cppGamma;
std::memcpy(cppGamma.data(), gamma, ProofGamma::Size);

// 2. call c++ function
auto cppHash512 = GenerateVrfProofHash(cppGamma);

// 3. copy result
std::memcpy(hash512, cppHash512.data(), cppHash512.size());

BUILD NOTES

CFFI は、スタティック C ライブラリ、ダイナミック C ライブラリの両方と連携して動作します。C++のライブラリに対してリンクする必要がありますが、これはCリンカーではできません。これを回避するために、ダイナミックライブラリを使用する必要があります。C++の依存関係は、ダイナミックライブラリのビルド時にすべて解決されます。CFFIは、Cファンクションラッパーに対してのみリンクする必要があり、それは可能です。

catapultクライアントビルドシステムでは、以下の手順でダイナミックライブラリをビルドすることができます。

catapult_shared_library_target(catapult.cvrf)
target_link_libraries(catapult.cvrf catapult.crypto)

さておき。catapult.cryptoライブラリは、今回呼び出すVRF関数が含まれているため、リンクする必要があります。

さらに、ダイナミックライブラリからエクスポートしたい関数として、すべてのC関数をマークする必要があり、そのためにPLUGIN_APIマクロを使用しています。:こちら

ダイナミックライブラリを構築する際、エクスポートされるすべての関数がC言語の呼び出し規約を使用していることを確認する必要があります。そのためには、extern "C" ブロックでラップする必要がある。

#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif

extern "C" ブロックは条件付きで、C++コンパイラでビルドするときだけ含まれることに注意してください。CコンパイラはC呼び出しの規約を使うので、このブロックは冗長である(認識されない)。

BUILDING

ソースからビルドするには、このガイドに従って _build ディレクトリを作成します。ステップ 3 の最後の ninja の呼び出し (ブランチ全体をビルドする) を省略すると、より快適に使用できます。代わりに、ninja catapult.cvrf コマンドを使用します (これは VRF C interop dll とその依存関係のみをビルドします)。

CFFI (BUILD)

C言語のAPIが手に入ったので、CFFIを使ってPythonで呼び出し可能なラッパーを作成する必要があります。

まず、FFIビルダーを作成する必要があります。

from cffi import FFI

ffi_builder = FFI()

簡単のために、catapultクライアントのソースコードを指す環境変数が存在すると仮定します。

catapult_client_root = Path(os.environ.get('CATAPULT_CLIENT_ROOT'))
catapult_default_bin_directory = catapult_client_root / '_build' / 'bin'

次に、set_source を呼び出して、ビルダーに私たちのコードとその依存関係を指し示す必要があります。

  1. 出力するPythonモジュールの名前を_vrfに設定します(この名前は、Pythonコードのimport文で使用されます)。

  2. 全てのC関数を含むshimヘッダをインクルードします。

  3. catapultクライアントのroodディレクトリから相対的にincludeとlibraryディレクトリをセットアップします。

  4. リンクするC関数を含むライブラリの名前(catapult.cvrf)を指定します。

ffi_builder.set_source(
	'_vrf',
	r'''
		#include "VrfShim.h"
	''',
	include_dirs = [
		catapult_client_root / 'examples' / 'vrfinterop' / 'cdll',
		catapult_client_root / 'src'
	],
	library_dirs = [str(catapult_default_bin_directory)],
	libraries=['catapult.cvrf'],
	extra_link_args=extra_link_args)

余談: 特定の*nixオペレーティングシステムでは、実行時にダイナミックライブラリを見つけられるように、RPATHを追加で設定する必要があります。そのためには、以下のコードを使用します。

if 'Darwin' == os.uname().sysname:
	extra_link_args += ['-rpath', str(catapult_default_bin_directory)]
	boost_lib_bin_directory = os.environ.get('BOOST_BIN_DIRECTORY', None)
	if boost_lib_bin_directory:
		extra_link_args += ['-rpath', str(boost_lib_bin_directory)]

BOOST_BIN_DIRECTORY は、boost ダイナミック・ライブラリーが catapult ダイナミック・ライブラリーと同じディレクトリーにない場合、そのディレクトリーに設定する必要があります。

次に、Pythonから呼び出せるようにしたい構造体や関数を指定します。この例では、すべての構造体と関数を呼び出し可能にしたいのです。そのためには、cdefコールでそれらの宣言を指定する必要があります。関数は構造体に依存している(すなわち、CVrfProofパラメータを持っている)ので、2つの呼び出しが必要です。

ffi_builder.cdef('''
	struct CVrfProof {
		unsigned char Gamma[32];
		unsigned char VerificationHash[16];
		unsigned char Scalar[32];
	};
''')

ffi_builder.cdef('''
	void CatapultGenerateVrfProof(
			const unsigned char* alpha,
			unsigned int alphaSize,
			const unsigned char* privateKey,
			struct CVrfProof* vrfProof);

	void CatapultVerifyVrfProof(
			const struct CVrfProof* vrfProof,
			const unsigned char* alpha,
			unsigned int alphaSize,
			const unsigned char* publicKey,
			unsigned char* hash512);

	void CatapultGenerateVrfProofHash(const unsigned char* gamma, unsigned char* hash512);
''')

最後に、CFFIモジュールをコンパイルするためのデフォルトスクリプトアクションを設定する必要があります。

if '__main__' == __name__:
	ffi_builder.compile(verbose=True)

Pythonファイルを実行すると、他のPythonスクリプトでインポート可能な_vrfファイルが生成されるはずです。

BUILDING

vrf interop dynamic library を構築するために、以下のコマンドを実行します。

cd examples/vrfinterop
pip install -r requirements.txt
CATAPULT_CLIENT_ROOT=../.. python -m _cffi.vrf_build

成功すると、_vrfで始まるファイルが生成されるはずです。

PYTHON

私たちが行ったことすべてがうまくいくことを証明するために、私たちのクライアントリファレンス実装を検証するために使用したVRFテストベクターを検証する短いPythonスクリプトを書きます。これは、常に公式のテストベクタを使用することを思い出させる良い機会です。

念のため、テストベクトルをここに再現しておく。

TestCaseInput = namedtuple('TestCaseInput', ['private_key', 'alpha'])
TestCaseOutput = namedtuple('TestCaseInput', ['gamma', 'verification_hash', 'scalar', 'beta'])
TestCase = namedtuple('TestCase', ['input', 'output'])

test_cases = [
	TestCase(
		TestCaseInput('9D61B19DEFFD5A60BA844AF492EC2CC44449C5697B326919703BAC031CAE7F60', ''),
		TestCaseOutput(
			'9275DF67A68C8745C0FF97B48201EE6DB447F7C93B23AE24CDC2400F52FDB08A',
			'1A6AC7EC71BF9C9C76E96EE4675EBFF6',
			'0625AF28718501047BFD87B810C2D2139B73C23BD69DE66360953A642C2A330A',
			'A64C292EC45F6B252828AFF9A02A0FE88D2FCC7F5FC61BB328F03F4C6C0657A9D26EFB23B87647FF54F71CD51A6FA4C4E31661D8F72B41FF00AC4D2EEC2EA7B3'
		)
	),
	TestCase(
		TestCaseInput('4CCD089B28FF96DA9DB6C346EC114E0F5B8A319F35ABA624DA8CF6ED4FB8A6FB', '72'),
		TestCaseOutput(
			'84A63E74ECA8FDD64E9972DCDA1C6F33D03CE3CD4D333FD6CC789DB12B5A7B9D',
			'03F1CB6B2BF7CD81A2A20BACF6E1C04E',
			'59F2FA16D9119C73A45A97194B504FB9A5C8CF37F6DA85E03368D6882E511008',
			'CDDAA399BB9C56D3BE15792E43A6742FB72B1D248A7F24FD5CC585B232C26C934711393B4D97284B2BCCA588775B72DC0B0F4B5A195BC41F8D2B80B6981C784E'
		)
	),
	TestCase(
		TestCaseInput('C5AA8DF43F9F837BEDB7442F31DCB7B166D38535076F094B85CE3A2E0B4458F7', 'af82'),
		TestCaseOutput(
			'ACA8ADE9B7F03E2B149637629F95654C94FC9053C225EC21E5838F193AF2B727',
			'B84AD849B0039AD38B41513FE5A66CDD',
			'2367737A84B488D62486BD2FB110B4801A46BFCA770AF98E059158AC563B690F',
			'D938B2012F2551B0E13A49568612EFFCBDCA2AED5D1D3A13F47E180E01218916E049837BD246F66D5058E56D3413DBBBAD964F5E9F160A81C9A1355DCD99B453'
		)
	),
]

さらに、バッファを受け取り、16進文字列を返すヘルパー関数を追加します。

def to_hex_string(buffer):
	return hexlify(bytes(buffer)).upper().decode('utf8')

CFFIで作成したモジュールのインポートはとても簡単です。ffiはメモリ管理/相互運用に使用され、libにはインポートした関数が格納されます。

from _vrf import lib, ffi

CATAPULTGENERATEVRFPROOF

ffi を用いて,関数に渡す CVrfProof のインスタンスを作成する必要があります.alpha と private_key は,TestCaseInput から抽出できます.

alpha = unhexlify(test_case.input.alpha)
private_key = PrivateKey(test_case.input.private_key)
vrf_proof = ffi.new('struct CVrfProof *');
lib.CatapultGenerateVrfProof(alpha, len(alpha), private_key.bytes, vrf_proof)

CVrfProof フィールドは Python に直接マッピングされ、直接アクセスできることに注意してください。これを知っていれば、TestCaseOutput にある期待される出力と比較することができます。

assert test_case.output.gamma == to_hex_string(vrf_proof.Gamma)
assert test_case.output.verification_hash == to_hex_string(vrf_proof.VerificationHash)
assert test_case.output.scalar == to_hex_string(vrf_proof.Scalar)

CATAPULTVERIFYVRFPROOF

秘密鍵から公開鍵を導出する必要があります。さらに、出力ハッシュを保持するバイトプレースホルダを作成する必要があります。

public_key = KeyPair(private_key).public_key
proof_hash = bytes(64)
lib.CatapultVerifyVrfProof(vrf_proof, alpha, len(alpha), public_key.bytes, proof_hash);

証明ハッシュと期待される証明ハッシュ(β)を比較することができる。

assert test_case.output.beta == to_hex_string(proof_hash_out)

CATAPULTGENERATEVRFPROOFHASH

vrf_proof Gammaフィールドは直接渡すことができます。もう一度言いますが、出力ハッシュを保持するバイトプレースホルダが必要です。

proof_hash_2 = bytes(64)
lib.CatapultGenerateVrfProofHash(vrf_proof.Gamma, proof_hash_2)

上記と同様に、証明ハッシュと期待される証明ハッシュ(β)を比較することができます。

assert test_case.output.beta == to_hex_string(proof_hash_2)

RUNNING

cd examples/vrfinterop
python -m example

余談:Library not loaded: '@rpath/libboost_date_time.dylib のようなエラーが出る場合、環境変数 BOOST_BIN_DIRECTORY を設定して CFFI のダイナミックライブラリを再構築する必要があります。それと、おそらくMacOSをお使いの方だと思います。

追記

これで、pythonからcatapultクライアント関数を呼び出す方法を学びました!

この素晴らしい新しいパワーで何ができるか考えてみてください。しかし、覚えておいてください。

この記事が気に入ったらサポートをしてみませんか?