自分の得意な言語でSymbolブロックチェーンを使いこなす方法


前回の投稿でsdkに依存せずにSymbolブロックチェーンで送信を行う方法を解説しました。

https://qiita.com/nem_takanobu/items/09b6b687f3cbe10af945

今回はアグリゲートトランザクションを実行する方法を解説します。
アグリゲートトランザクションを実行することができれば、Symbolブロックチェーンで提供されるほぼすべての機能を使用することができます。

(追記) あらゆる言語で共通の手順でトランザクションを生成できる関数群tsunagi-functionsを公開しました。

https://qiita.com/nem_takanobu/items/4b5d67fe9eb80ae35433

以下の順で説明していきます。

- catbuffer定義
- payload例
- オフライン署名（コンプリートトランザクション）
- ボンデッドトランザクション


必ず前回の記事の内容を理解したうえでお読みください。
内容がかぶる部分は省略しています。

# catbuffer定義

Transferトランザクションを内包するアグリゲートトランザクションの構成例です。
structはここに定義されています。

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

全体を展開すると以下のような構造になっています（転送トランザクションを埋め込んだ場合）

```text
//トランザクション
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 AggregateTransactionBody
	transactions_hash = Hash256
	payload_size = uint32
	aggregate_transaction_header_reserved_1 = make_reserved(uint32, 0)
	@is_byte_constrained
	@alignment(8)

    //埋め込みトランザクションリスト
	transactions = array(EmbeddedTransaction, payload_size)

        //埋め込みトランザクション
		inline EmbeddedTransaction
			inline EmbeddedTransactionHeader
				inline SizePrefixedEntity
					size = uint32
				embedded_transaction_header_reserved_1 = make_reserved(uint32, 0)
			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

        //転送トランザクション本体
		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)
    //連署情報
    cosignatures = array(Cosignature, __FILL__)
        version = uint64
        signer_public_key = PublicKey
        signature = Signature
```
# payload例

以下の転送トランザクションを内包するアグリゲートトランザクションのペイロード例です。
送信者(起案者):Alice-> 受信者:Bob
送信者(連署者):Bob-> 受信者:Carol
送信者(連署者):Carol->受信者:Alice

Aliceがトランザクションを作成（起案）し、BobとCarolがそのトランザクションに連署します。


```text
b002000000000000 //size reserved1
ADA21D9525AD83B6 //signature
210001720FC1449F
5238DE8BE829E143
A0562D81356F9DFD
C5E68A31BEB9BA4C
A8EC1B4475E23212
28D3313F4E6187D2
7CDAAB554CB69F0C
5F594DFC01857866 //signer
2E0B5A2F5F83ECFB
1CDA2B32E29FF1D9
B2C5E7325C4CF7CB
0000000001984141 //reserved2 version networkType txType
C00C010000000000 //fee
5F1D6D9F03000000 //deadline
6E3EF7D6760F5E3D //transactions_hash
BB76C5DF7FE955E3
8B1A095EEA3C3FBD
E43AA80EB16E75A6
3801000000000000 //payload_size reserved
					//embededd1
6400000000000000		size reserved
5F594DFC01857866		signer_public_key
2E0B5A2F5F83ECFB
1CDA2B32E29FF1D9
B2C5E7325C4CF7CB
0000000001985441		reserved  version networkType txType
9877A85EEA84B376		recipientAddress
A46721480A2A8B74
A5EC750B6F009B72
0400010000000000		messageSize mosaicCount txReserved
C8B6532DDB16843A		mosaic id
40420F0000000000		mosaic amount
0074783100000000		message
					//embededd2
6400000000000000		size reserved
13B2217BADDA5B03		signer_public_key
44AF1D17DAC36CF7
78604452079B2EC1
E3E84D241D8DB97C
0000000001985441		reserved  version networkType txType
98945BEB5280F41B		recipientAddress
49744551C28AFF71
EFFAE4FB69DAEA35
0400010000000000		messageSize mosaicCount txReserved
C8B6532DDB16843A		mosaic id
40420F0000000000		mosaic amount
0074783200000000		message
					//embededd2
6400000000000000		size reserved
8A42265D4644D407		signer_public_key
B75D8B3D3E00807F
CE993D7DCEF0494D
B05A02439509A66F
0000000001985441		reserved  version networkType txType
9877A85EEA84B376		recipientAddress
A46721480A2A8B74
A5EC750B6F009B72
0400010000000000		messageSize mosaicCount txReserved
C8B6532DDB16843A		mosaic id
40420F0000000000		mosaic amount
0074783100000000		message
					//cosignature1
0000000000000000		version
13B2217BADDA5B03		publicKey
44AF1D17DAC36CF7
78604452079B2EC1
E3E84D241D8DB97C
9A5BCBD12B5A1A21		cosignature
9DE810A922E26E9A
234D33148D409263
EEF288E5C913BCA7
2CAE687F1B0AA74C
E5660EAC073B10EE
3CB16C44C64B6505
FCA4E5FB8450A200
					//cosignature2
0000000000000000		version
8A42265D4644D407		publicKey
B75D8B3D3E00807F
CE993D7DCEF0494D
B05A02439509A66F
3C92CE96ADBC6BE5		cosignature
FCE3A0EE9FFE2854
75688930BE86EC32
360D3A81E53FD72D
830C0CD223AD8DD3
3C3C22CF427FD787
4D94F73AF28C0D63
4B81FDA0BCC6C007
```
# オフライン署名

オフライン署名では、関係するアカウントの署名をすべて集めてからトランザクションをブロックチェーンに通知します。トランザクションペイロードの最後に連署者情報を追記します。


#### embeddedトランザクションの生成
```js
function createEmbeddedTransferTx(signerBytes,recipientAddressBase32){

    embeddedTransactionHeaderReserved
                 = Buffer.from(new Uint32Array([0]).buffer); //0固定
    signer       = signerBytes; //署名者の公開鍵
    entityBodyReserved
                 = 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固定
    recipientAddress = Buffer.from(base32.decode(recipientAddressBase32 + "A").slice(0, -1)); //送信先アドレス
    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!')]); //メッセージ

    messageSize  = Buffer.from(new Uint16Array([message.length]).buffer); //メッセージの長さ
    txSize = Buffer.from(new Uint32Array([96 + message.length]).buffer);

    embeddedTx = [
        ...txSize,
        ...embeddedTransactionHeaderReserved,
        ...signer,
        ...entityBodyReserved,
        ...version,
        ...networkType,
        ...txType,
        ...recipientAddress,
        ...messageSize,
        ...mosaicCount,
        ...txReserved,
        ...mosaicId,
        ...mosaicAmount,
        ...message
    ];

    return embeddedTx;
}

aliceSigner = Buffer.from('5f594dfc018578662e0b5a2f5f83ecfb1cda2b32e29ff1d9b2c5e7325c4cf7cb','hex'); //aliceの公開鍵
bobSigner   = Buffer.from('6199BAE3B241DF60418E258D046C22C8C1A5DE2F4F325753554E7FD9C650AFEC','hex'); //bobの公開鍵
carolSigner = Buffer.from('886ADFBD4213576D63EA7E7A4BECE61C6933C27CD2FF36F85155C8FEBFB6EB4E','hex'); //carolの公開鍵

etTx1 = createEmbeddedTransferTx(aliceSigner,"TCO7HLVDQUX6V7C737BCM3VYJ3MKP6REE2EKROA"); //alice->bob
etTx2 = createEmbeddedTransferTx(  bobSigner,"TDZBCWHAVA62R4JFZJJUXQWXLIRTUK5KZHFR5AQ"); //bob->carol
etTx3 = createEmbeddedTransferTx(carolSigner,"TBUXMJAYYW3EH3XHBZXSBVGVKXKZS4EH26TINKI"); //carol->alice
```

#### マークルハッシュの作成

埋め込まれたトランザクション(EmbeddedTransaction)を要約するハッシュ値を作成します。

アグリゲートトランザクションはトランザクション全体に対して署名しません。埋め込んだトランザクションのマークルハッシュ値を計算して、それを署名対象とします。
ここでは、マークルハッシュの作り方を説明します。

```js
hashes = [];
hashes.push(sha3_256.create().update(etTx1).digest());
hashes.push(sha3_256.create().update(etTx2).digest());
hashes.push(sha3_256.create().update(etTx3).digest());
let numRemainingHashes = hashes.length;
while (1 < numRemainingHashes) {
    let i = 0;
    while (i < numRemainingHashes) {
        const hasher = sha3_256.create();
        hasher.update(hashes[i]);

        if (i + 1 < numRemainingHashes) {
            hasher.update(hashes[i + 1]);
        } else {
            // if there is an odd number of hashes, duplicate the last one
            hasher.update(hashes[i]);
            numRemainingHashes += 1;
        }
        hashes[Math.trunc(i / 2)] = hasher.digest();
        i += 2;
    }
    numRemainingHashes = Math.trunc(numRemainingHashes / 2);
}
```
#### 署名
トランザクションを作成したアカウント（起案者）によって署名します。

```js
version      = Buffer.from(new Uint8Array([1]).buffer); //現在1固定
networkType  = Buffer.from(new Uint8Array([152]).buffer); //testnet 152固定
txType       = Buffer.from(new Uint16Array([16705]).buffer); //コンプリートトランザクション
fee          = Buffer.from(new BigInt64Array([1000000n]).buffer); //最大手数料
deadline     = Buffer.from(new BigInt64Array([deadline]).buffer); //有効期限

verifiableData = [
...version,
...networkType,
...txType,
...fee,
...deadline,
...Buffer.from(hashes[0]) //transactionsHashBytes
];

aliceKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7")
);

signature = aliceKeypair.sign(new Uint8Array([
...Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"),
...verifiableData
]));
```
keypairで署名する部分は各言語のed25519ライブラリ(salt、sodium等)をお使いください。
本稿ではすでにsdkのあるJSで疑似コードで説明しているため、sdkを利用した署名としています。

#### 連署

連署者Bob、Carolの署名を作成します。
Aliceが作成したトランザクションのハッシュ値を計算し、その値に対して署名を行うことで連署したものとみなされます。

```js
hasher = sha3_256.create();
hasher.update(signature.bytes);
hasher.update(aliceSigner);
hasher.update(Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"));
hasher.update(verifiableData);
txHash = hasher.hex();

function createCosignature(keypair,parentHash){

    return [
    ...Buffer.from(new BigInt64Array([0n]).buffer), //version
    ...keypair.publicKey.bytes, //publicKey
    ...keypair.sign(new Uint8Array(Buffer.from(parentHash,"hex"))).bytes //signature
    ];
}

bobKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("fa6373f4f497773c5cc55c103e348b139461d61fd4b45387e69d08a68000e06b")
);

carolKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("1e090b2a266877a9f88a510af2eb0945a63dc69dbce674ccd83272717d4175cf")
);
```

#### トランザクション生成

必要なデータをそろえて、トランザクションを作成します。

```js
function alignUp(embeddedTx){
    alignedSize = Math.floor((embeddedTx.length + 8 - 1)/ 8 ) * 8;
    if (alignedSize - embeddedTx.length)
        paddingArray = new Uint8Array(alignedSize - embeddedTx.length);
        alignedEmbeddedTx = [...embeddedTx,...paddingArray];
    return alignedEmbeddedTx;
}

embeddedTxes = [
...alignUp(etTx1),
...alignUp(etTx2),
...alignUp(etTx3),
];

txBuffer = [
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...signature.bytes,
...aliceSigner, 
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...verifiableData,  
...Buffer.from(new Uint32Array([embeddedTxes.length]).buffer),
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...embeddedTxes,
...createCosignature(bobKeypair,txHash), //cosignature
...createCosignature(carolKeypair,txHash) // cosignature
];

txSize       = Buffer.from(new Uint32Array([txBuffer.length + 4]).buffer); //トランザクションの長さ
txBuffer = [...txSize,...txBuffer];
```
埋め込みトランザクションが可変長のデータを持つ場合、8バイトで整列できるようにalignUpという処理で0埋めを行います。連署はトランザクションの最後に追加します。


#### ペイロード通知

ノードにトランザクションをJSON化したペイロードで通知します。

```js
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();
```

#### 確認

最後にトランザクションの承認状態を確認します。

```js
//承認状態の確認
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}`);

```

# ボンデッドトランザクション

ボンデッドトランザクションはAPIノードに署名が完成するまで、未完成（パーシャル）トランザクションとして留め置きしておく方法です。オフライン署名とは異なる部分のみ解説していきます。

```js
version      = Buffer.from(new Uint8Array([1]).buffer); //現在1固定
networkType  = Buffer.from(new Uint8Array([152]).buffer); //testnet 152固定
txType       = Buffer.from(new Uint16Array([16961]).buffer); //ボンデッドトランザクション
fee          = Buffer.from(new BigInt64Array([1000000n]).buffer); //最大手数料

verifiableData = [
...version,
...networkType,
...txType,
...fee,
...Buffer.from(new BigInt64Array([deadline]).buffer), //有効期限,
...Buffer.from(hashes[0]) //transactionsHashBytes
];

aliceKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7")
);

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

ボンデッドトランザクションのトランザクションタイプは16961になります。

#### トランザクション生成
```js
txBuffer = [
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...signature.bytes,
...aliceSigner, 
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...verifiableData,  
...Buffer.from(new Uint32Array([embeddedTxes.length]).buffer),
...Buffer.from(new Uint32Array([0]).buffer), //reserved
...embeddedTxes,
];

txSize       = Buffer.from(new Uint32Array([txBuffer.length + 4]).buffer); //トランザクションの長さ
txBuffer = [...txSize,...txBuffer];

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

連署者を含める必要はありません。

#### ハッシュロック

ここがオフライン署名とは大きく異なる点になります。
ボンデッドトランザクションは、あらかじめノードに対して留め置き（ハッシュロック）しておいてほしい旨を通知しておく必要があります。
ネットワーク手数料とは別に10XYMのデポジットが必要です。トランザクションが承認されればこの10XYMは返却されます。

```js

verifiableLockData = [
...Buffer.from(new Uint8Array([1]).buffer),
...Buffer.from(new Uint8Array([152]).buffer),
...Buffer.from(new Uint16Array([16712]).buffer),
...Buffer.from(new BigInt64Array([18400n]).buffer), //fee
...Buffer.from(new BigInt64Array([deadline]).buffer),
...Buffer.from(new BigInt64Array([BigInt("0x3A8416DB2D53B6C8")]).buffer),
...Buffer.from(new BigInt64Array([10000000n]).buffer), //モザイク数量
...Buffer.from(new BigInt64Array([480n]).buffer),
...new Uint8Array(hasher.arrayBuffer())
];

lockSignature = aliceKeypair.sign(new Uint8Array([
...Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"),
...verifiableLockData
])).bytes,

lockTxBuffer = [
...Buffer.from(new Uint32Array([verifiableLockData.length + 108]).buffer),
...Buffer.from(new Uint32Array([0]).buffer),
...lockSignature, //signature
...aliceSigner,//signer
...Buffer.from(new Uint32Array([0]).buffer),
...verifiableLockData
]

lockPayload = {
  payload:Buffer.from(lockTxBuffer).toString('hex')
}

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

lockHasher = sha3_256.create();
lockHasher.update(lockSignature);
lockHasher.update(aliceSigner);
lockHasher.update(Buffer.from("7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836","hex"));
lockHasher.update(verifiableLockData);
lockTxHash = lockHasher.hex();
```

オフライン署名の"連署"で作成したhasherをverifiableLockDataに埋め込んで署名します。


#### ハッシュロックの承認状態確認
```js
//承認状態の確認
res = await fetch(`${node}/transactionStatus/${lockTxHash}`);
console.log(await res.json());

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

//エクスプローラーで確認
console.log(`https://testnet.symbol.fyi/transactions/${lockTxHash}`);
```
#### アグリゲートトランザクション通知

ハッシュロックの承認が確認できたら、本体のボンデッドトランザクションをノードに通知します。

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

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

通知先エンドポイントは`/transactions/partial`になりますのでご注意ください。
承認状態がunconfirmedからpartialになれば成功です。

#### Bobの連署

partialトランザクションに対してBobの連署を通知します。

```js
bobKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("fa6373f4f497773c5cc55c103e348b139461d61fd4b45387e69d08a68000e06b")
);

payload = {
    parentHash:txHash,
    signature:Buffer.from(bobKeypair.sign(new Uint8Array(Buffer.from(txHash,"hex"))).bytes).toString("hex"),
    signerPublicKey:Buffer.from(bobKeypair.publicKey.bytes).toString("hex"),
    version:"0"
}

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

エンドポイントは `/transactions/cosignature` になるのでご注意ください。


#### Carolの連署
```js
carolKeypair = new sdk.facade.SymbolFacade.KeyPair(
  new sdk.CryptoTypes.PrivateKey("1e090b2a266877a9f88a510af2eb0945a63dc69dbce674ccd83272717d4175cf")
);

payload = {
    parentHash:txHash,
    signature:Buffer.from(carolKeypair.sign(new Uint8Array(Buffer.from(txHash,"hex"))).bytes).toString("hex"),
    signerPublicKey:Buffer.from(carolKeypair.publicKey.bytes).toString("hex"),
    version:"0"
}

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

#### 最終確認

Bob、Carolの連署が完了したら、最後に承認状態を確認します。


```js
//承認状態の確認
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に特化したライブラリを必要とすることなく、複雑なトランザクションを実行することができました。今回紹介したスクリプトはJSをベースにした模擬コードですが、今後さまざまな言語バージョンでの実装例をご紹介していきますのでご期待ください。