16
Help us understand the problem. What are the problem?

posted at

updated at

自分の得意なプログラミング言語でSymbolブロックチェーンを動かす方法

Symbolブロックチェーンはプログラミング言語を問わずスマートコントラクトが実装できるように設計されています。ところが、現在リリースされているsdkアルファ版はJavascriptとPythonのみサポートです。では、他の言語ではまだアプリ開発できないのでしょうか?と言われればそうでもありません。

組み込みされたデジタル装置で署名したかったり、マルチシグ化した連署者アカウントの署名をサーバ側で機械的に実施したり、IoTなどほぼ単一のトランザクションを定期的に実施したり、複雑なことは必要ないユースケースが実はたくさん眠っているかもしれません。

ここでは、そういった「あとはSymbolブロックチェーンにトランザクション投げるだけなんだが」といった方を対象にトランザクションの作り方を解説します。
なお、トランザクションの送信以外(例えばモザイクの生成やマルチシグの組成など)はウォレットなどを利用して準備する前提で進めます。

後記:
Symbol/NEMのコアチームからのSDKについての考え方を転載しておきます。

私たちは、コミュニティが(SDKではなく)直接RESTを使用することを望んでいます。暗号通貨SDKのデザインパターンは、ウォレットの機能とアドレスなどを扱うだけでよいというものです。私たちはOpenAPIライブラリを書くこともできますが、代わりにコミュニティがこれらのライブラリやツールを作ってくれることを望んでいます。
これらのデザインパターンは、ユーザーや新規開発者がSymbol 2.0で複数のSubChainを扱う際に遭遇するUXに備えることを目的としています。
コミュニティで開発されたTypeScript SDKがあると良いですね。Python SDKを参考にしながら、コミュニティがたくさんのSDKを開発することを期待します。

検証済みの言語を紹介しておきます。

最低限必要な機能

どんな言語でも実装できるとはいえ、これだけは必要と思われる機能について確認しておきます。

  • HTTPアクセス
    • プログラムからノードのエンドポイントにHTTPでアクセスする必要があります。必須ではありませんがWebSocketにも対応しているとトランザクションの承認状態等の監視が出来て便利です(今回は説明しません)
  • 署名
    • Ed25519の署名をサポートしたライブラリ(NaClやSodium)が必要です。古い言語はここが難しいかもしれません。
  • BigInt
    • SymbolブロックチェーンではIntegerで定義される数字よりも大きな数字を頻繁に扱います。最後には文字列にするので必ずしも必要ありませんが、トランザクションの有効期限などを現在時刻から加算計算する場合に数値として扱えると便利です。

トランザクションの送信先

現在、世界中に1200か所ほど分散して存在しているSymbol APIノードのエンドポイントに接続します。
以下のサイトから"https"対応と表示されたノードを選択して、署名されたトランザクションを送信します。
https://symbolnodes.org/nodes/

実際にpayloadをjson形式でPUTするエンドポイント /transactions の仕様については以下のAPIリファレンスを参照ください。
https://symbol.github.io/symbol-openapi/v1.0.3/#operation/announceTransaction

image.png

トランザクションの送信方法

上記エンドポイントに送信するデータはこんなフォーマットになっています。

payload = '{"payload":"A000000000000000934A1630CD8749708872E11229033C6A78BD3CFC53E9C62223875427A950390F01E9F7D4BC3480F06BA4777508A59BB72C3ECE0963E238DB9BE4AD823534B90F0E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D260000000001985441803E000000000000B4652371030000009850BF0FD1A45FCEE211B57D0FE2B6421EB81979814F62920000000000000000"}'

JavaScriptで送信する場合以下のような書き方になります。

response = await fetch(`${node}/transactions`, {
	method: 'put',
	body: payload,
	headers: {'Content-Type': 'application/json'}
});
await response.json();

トランザクションのデータ構成

以下のpayloadの構成を調べてみます。

BA0000000000000073F13B93649BF187DA16500B3EB5EE4ADD58CF88F20A8FC9061A00F4ADEB04B8C67FEE785BB832A0D4A28D28FF940B1189CD231875A6420822C16AF37AF68F000E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D260000000001985441A848000000000000709CE07D0300000098F96BD2F803DE1EE39AACFC53A246F4F7A46901A5D0A53E0A00010000000000C8B6532DDB16843A40420F000000000000476F6F644C75636B21

16文字ごとに改行すると以下のような構成を取っているのがわかります。

BA00000000000000 //size reserved1
73F13B93649BF187 //signature
DA16500B3EB5EE4A
DD58CF88F20A8FC9
061A00F4ADEB04B8
C67FEE785BB832A0
D4A28D28FF940B11
89CD231875A64208
22C16AF37AF68F00
0E5C72B0D5946C1E //signer
FEE7E5317C5985F1
06B739BB0BC07E4F
9A288417B3CD6D26
0000000001985441 //reserved2 version networkType txType
A848000000000000 //deadline
709CE07D03000000 //fee
98F96BD2F803DE1E //recipientAddress
E39AACFC53A246F4
F7A46901A5D0A53E
0A00010000000000 //messageSize mosaicCount txReserved
C8B6532DDB16843A //mosaicId
40420F0000000000 //mosaicAmount
00476F6F644C7563 //message
6B21

トランザクションのデータ構成仕様

catbuffer/schemas と呼ばれるレポジトリで定義されています。
SDKはこのスキーマを参考にして自動生成されています。

今回解説する転送トランザクションTRANSFERのアウトラインは以下のファイルで定義されています。

transfer/transfer.cats
https://github.com/symbol/symbol/blob/dev/catbuffer/schemas/symbol/transfer/transfer.cats

struct TransferTransaction
	TRANSACTION_VERSION = make_const(uint8, 1)
	TRANSACTION_TYPE = make_const(TransactionType, TRANSFER)

	inline Transaction
	inline TransferTransactionBody

inlineで列記された TransactionTransferTransactionBodyで構成されることが分かります。
TransferTransactionBodyについては同じファイルに記載されています。

inline struct TransferTransactionBody
	# recipient address
	recipient_address = UnresolvedAddress

	# size of attached message
	message_size = uint16

	# number of attached mosaics
	mosaics_count = uint8

	# reserved padding to align mosaics on 8-byte boundary
	transfer_transaction_body_reserved_1 = make_reserved(uint8, 0)

	# reserved padding to align mosaics on 8-byte boundary
	transfer_transaction_body_reserved_2 = make_reserved(uint32, 0)

	# attached mosaics
	@sort_key(mosaic_id)
	mosaics = array(UnresolvedMosaic, mosaics_count)

	# attached message
	message = array(uint8, message_size)

よく分からない型が出てきたらここに記載されていないか調べてみましょう。
https://github.com/symbol/symbol/blob/dev/catbuffer/schemas/symbol/types.cats

次にTransactionが記載されている以下のファイルを見てみます。

transaction.cats
https://github.com/symbol/symbol/blob/dev/catbuffer/schemas/symbol/transaction.cats

トランザクション全般の構成が記載されています。

abstract struct Transaction
	inline SizePrefixedEntity
	inline VerifiableEntity
	inline EntityBody

	# transaction type
	type = TransactionType

	# transaction fee
	fee = Amount

	# transaction deadline
	deadline = Timestamp

SizePrefixedEntity,VerifiableEntity,EntityBodyについては以下のファイルに構成が記載されています。

entity.cats
https://github.com/symbol/symbol/blob/dev/catbuffer/schemas/symbol/entity.cats

このようにして、トランザクションに必要なデータやその構成をcatbufferで知ることができます。
結果として転送トランザクションの構成は以下の通りとなります。

inline Transaction
	inline SizePrefixedEntity
		size = uint32
	inline VerifiableEntity
		verifiable_entity_header_reserved_1 = make_reserved(uint32, 0)
		signature = Signature
			using Signature = binary_fixed(64)
	inline EntityBody
		signer_public_key = PublicKey
			using PublicKey = binary_fixed(32)
		entity_body_reserved_1 = make_reserved(uint32, 0)
		version = uint8
		network = NetworkType
			enum NetworkType : uint8
	type = TransactionType
		enum TransactionType : uint16
	fee = Amount
		using Amount = uint64
	deadline = Timestamp
		using Timestamp = uint64
inline TransferTransactionBody
	recipient_address = UnresolvedAddress
		using UnresolvedAddress = binary_fixed(24)
	message_size = uint16
	mosaics_count = uint8
	transfer_transaction_body_reserved_1 = make_reserved(uint8, 0)
	transfer_transaction_body_reserved_2 = make_reserved(uint32, 0)
	@sort_key(mosaic_id)
		mosaics = array(UnresolvedMosaic, mosaics_count)
			mosaic_id = UnresolvedMosaicId
				using UnresolvedMosaicId = uint64
			amount = Amount
				using Amount = uint64
	message = array(uint8, message_size)

トランザクションデータ作成

では実際にトランザクションデータを作成していきましょう。

txSize       = Buffer.from(new Uint32Array([0]).buffer); //トランザクションの長さ
reserved1    = Buffer.from(new Uint32Array([0]).buffer); //0固定
signature = Buffer.from('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000','hex');
signer       = Buffer.from('0e5c72b0d5946c1efee7e5317c5985f106b739bb0bc07e4f9a288417b3cd6d26','hex'); //署名者の公開鍵
reserved2    = Buffer.from(new Uint32Array([0]).buffer); //0固定

version      = Buffer.from(new Uint8Array([1]).buffer); //現在1固定
networkType  = Buffer.from(new Uint8Array([152]).buffer); //testnet 152固定
txType       = Buffer.from(new Uint16Array([16724]).buffer); //転送トランザクションは16724固定
fee          = Buffer.from(new BigInt64Array([16000n]).buffer); //最大手数料
deadline     = Buffer.from(new BigInt64Array([14834924187n]).buffer); //有効期限
recipientAddress = Buffer.from(base32.decode("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ" + "A").slice(0, -1)); //送信先アドレス
messageSize  = Buffer.from(new Uint16Array([14]).buffer); //メッセージの長さ
mosaicCount  = Buffer.from(new Uint8Array([1]).buffer); //送信するモザイクの種類
txReserved   = [...Buffer.from(new Uint8Array([0]).buffer),...Buffer.from(new Uint32Array([0]).buffer)]; //0固定
mosaicId     = Buffer.from(new BigInt64Array([BigInt("0x3A8416DB2D53B6C8")]).buffer); //モザイクID
mosaicAmount = Buffer.from(new BigInt64Array([100n]).buffer); //モザイク数量
message      = Buffer.from([0,...(new TextEncoder('utf-8')).encode('Hello Symbol!')]); //メッセージ

deadline

Symbolでは初めてブロックが生成された時点を基準にネットワーク時計が存在しています。
deadlineもその基準に合わせて指定する必要があります。

epochAdjustment = 1637848847; //テストネットの誕生時刻 Thu Nov 25 2021 23:00:47 GMT+0900 (日本標準時)
secs = 7200; //2時間後を期限とする場合
value = ((Math.trunc(Date.now() / 1000) + secs) - epochAdjustment) * 1000;
deadline = BigInt(value);

リトルエンディアン

数値はすべてリトルエンディアン(桁数の小さいものを左に)指定してください。
MosaicIdも内部ではBigIntで保持しているのでリトルエンディアンで指定する必要があります。

recipientAddress

アドレスはBase32エンコードされているのでdecodeする必要があります。
Base32の仕様上、アドレスの最後に"A"を付けてデコード後の配列から末尾を除去してください。
言語によってはそのような作業が必要ないライブラリが提供されている場合もあります。

recipientAddress = Buffer.from(base32.decode("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ" + "A").slice(0, -1)); //送信先アドレス

以下のように署名する部分のみ結合します。

verifiableData = [
...version,
...networkType,
...txType,
...fee,
...deadline,
...recipientAddress,
...messageSize,
...mosaicCount,
...txReserved,
...mosaicId,
...mosaicAmount,
...message
];

署名

作成したトランザクションを署名します。
この部分は各言語によって書き方が大きく異なると思います。
ぜひ得意言語のライブラリからEd25519の署名を探してみてください。

signature = keyPair.sign(new Uint8Array([
	...Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"),
	...verifiableData
]));

generationHashと呼ばれる最初にネットワークを作るときに定義したシード値をトランザクションの検証部分に付与して署名します。
testnetは7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836です。

検算

上記verifiableDataを下記で作成したkeypairで署名した場合の出力を記載しておきます。

Buffer.from([
	...Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"),
	...verifiableData
]).toString("hex");

署名対象となるペイロード

> 7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e83601985441803e0000000000009bfa3a74030000009850bf0fd1a45fcee211b57d0fe2b6421eb81979814f62920e00010000000000c8b6532ddb16843a64000000000000000048656c6c6f2053796d626f6c21
sym.KeyPair.createKeyPairFromPrivateKeyString("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7");
signature = sym.KeyPair.sign(keypair,new Uint8Array([
	...Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"),
	...verifiableData
]));

署名結果

Buffer.from(signature).toString("hex");
> '86221d79b2c63cb5385ff4a90820f69b28a8759d99ea39f70c168ef1e66174cce3e99862f0ca71a253b4c7b697b0dfa751d63937f07c62176664c5f36ce59b0a'

jsonフォーマットでpayloadを作成

txSize = Buffer.from(new Uint32Array([verifiableData.length + 108]).buffer);
txBuffer = [
...txSize,
...reserved1,
...signature,
...signer,
...reserved2,
...verifiableData
]

hexPayload = Buffer.from(txBuffer).toString('hex');
payload = `{"payload": "${hexPayload}"}`;

トランザクション送信

以下のコマンドでトランザクションを送信します。

response = await fetch(`${node}/transactions`, {
	method: 'put',
	body: payload,
	headers: {'Content-Type': 'application/json'}
})
await response.text()

payloadがノードに受理されればstatus:200が返ってきますが、これはブロックチェーンに承認されたことを意味するものではありません。トランザクションが妥当であった場合のみ未承認トランザクションとしてブロックチェーンの検証対象となります。

トランザクションの確認

送信したトランザクションの行方はhash値で問い合わせることができます。
hash値の生成方法について説明します。

hasher = sha3_256.create();
hasher.update(signature);
hasher.update(signer);
hasher.update(Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"));
hasher.update(verifiableData);
txHash = hasher.hex();

//承認状態の確認
res = await fetch(`${node}/transactionStatus/${txHash}`);
console.log(await res.json());

//承認済みトランザクションの確認
res = await fetch(`${node}/transactions/confirmed/${txHash}`);
console.log(await res.json());

//エクスプローラーで確認
console.log(`https://testnet.symbol.fyi/transactions/${txHash}`);


さいごに

今回はSymbolブロックチェーンの最も単純な送金についてのみ解説しました。
Symbolブロックチェーンのアグリゲートトランザクションを使えば、一連の送受信をまとめて処理したり署名がそろうまでトランザクションの処理をロックしたり様々なスマートコントラクトが記述できるようになります。
また後日紹介するかもしれませんが、待てないという方はぜひCatbufferを調べてみてください。

追記:書きました。

Appendex

base32.decode


charMapping = {
	createBuilder: () => {
		const map = {};
		return {
			map,
			addRange: (start, end, base) => {
				const startCode = start.charCodeAt(0);
				const endCode = end.charCodeAt(0);

				for (let code = startCode; code <= endCode; ++code)
					map[String.fromCharCode(code)] = code - startCode + base;
			}
		};
	}
};

//https://github.com/symbol/symbol/blob/dev/sdk/javascript/src/utils/base32.js
DECODED_BLOCK_SIZE = 5;
ENCODED_BLOCK_SIZE = 8;

Char_To_Decoded_Char_Map = (() => {
	const builder = charMapping.createBuilder();
	builder.addRange('A', 'Z', 0);
	builder.addRange('2', '7', 26);
	return builder.map;
})();

decodeChar = c => {
	const decodedChar = Char_To_Decoded_Char_Map[c];
	if (undefined !== decodedChar)
		return decodedChar;

	throw Error(`illegal base32 character ${c}`);
};

decodeBlock = (input, inputOffset, output, outputOffset) => {
	const bytes = new Uint8Array(ENCODED_BLOCK_SIZE);
	for (let i = 0; i < ENCODED_BLOCK_SIZE; ++i)
		bytes[i] = decodeChar(input[inputOffset + i]);

	output[outputOffset + 0] = (bytes[0] << 3) | (bytes[1] >> 2);
	output[outputOffset + 1] = ((bytes[1] & 0x03) << 6) | (bytes[2] << 1) | (bytes[3] >> 4);
	output[outputOffset + 2] = ((bytes[3] & 0x0F) << 4) | (bytes[4] >> 1);
	output[outputOffset + 3] = ((bytes[4] & 0x01) << 7) | (bytes[5] << 2) | (bytes[6] >> 3);
	output[outputOffset + 4] = ((bytes[6] & 0x07) << 5) | bytes[7];
};

base32 = {

	decode: encoded => {
		if (0 !== encoded.length % ENCODED_BLOCK_SIZE)
			throw Error(`encoded size must be multiple of ${ENCODED_BLOCK_SIZE}`);

		const output = new Uint8Array(encoded.length / ENCODED_BLOCK_SIZE * DECODED_BLOCK_SIZE);
		for (let i = 0; i < encoded.length / ENCODED_BLOCK_SIZE; ++i)
			decodeBlock(encoded, i * ENCODED_BLOCK_SIZE, output, i * DECODED_BLOCK_SIZE);

		return output;
	}
};

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
16
Help us understand the problem. What are the problem?