前回の投稿でsdkに依存せずにSymbolブロックチェーンで送信を行う方法を解説しました。
今回はアグリゲートトランザクションを実行する方法を解説します。
アグリゲートトランザクションを実行することができれば、Symbolブロックチェーンで提供されるほぼすべての機能を使用することができます。
(追記) あらゆる言語で共通の手順でトランザクションを生成できる関数群tsunagi-functionsを公開しました。
以下の順で説明していきます。
- catbuffer定義
- payload例
- オフライン署名(コンプリートトランザクション)
- ボンデッドトランザクション
必ず前回の記事の内容を理解したうえでお読みください。
内容がかぶる部分は省略しています。
catbuffer定義
Transferトランザクションを内包するアグリゲートトランザクションの構成例です。
structはここに定義されています。
全体を展開すると以下のような構造になっています(転送トランザクションを埋め込んだ場合)
//トランザクション
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がそのトランザクションに連署します。
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トランザクションの生成
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)を要約するハッシュ値を作成します。
アグリゲートトランザクションはトランザクション全体に対して署名しません。埋め込んだトランザクションのマークルハッシュ値を計算して、それを署名対象とします。
ここでは、マークルハッシュの作り方を説明します。
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);
}
署名
トランザクションを作成したアカウント(起案者)によって署名します。
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が作成したトランザクションのハッシュ値を計算し、その値に対して署名を行うことで連署したものとみなされます。
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")
);
トランザクション生成
必要なデータをそろえて、トランザクションを作成します。
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化したペイロードで通知します。
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();
確認
最後にトランザクションの承認状態を確認します。
//承認状態の確認
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ノードに署名が完成するまで、未完成(パーシャル)トランザクションとして留め置きしておく方法です。オフライン署名とは異なる部分のみ解説していきます。
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になります。
トランザクション生成
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は返却されます。
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に埋め込んで署名します。
ハッシュロックの承認状態確認
//承認状態の確認
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}`);
アグリゲートトランザクション通知
ハッシュロックの承認が確認できたら、本体のボンデッドトランザクションをノードに通知します。
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の連署を通知します。
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の連署
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の連署が完了したら、最後に承認状態を確認します。
//承認状態の確認
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をベースにした模擬コードですが、今後さまざまな言語バージョンでの実装例をご紹介していきますのでご期待ください。