アグリゲートトランザクションを実行するための3つの方法


今回はSymbol from NEM,mijin Catapultの真骨頂、アグリゲートトランザクションについて説明します。

以下の記事を先に体験しておいてください。

アグリゲートトランザクションとは

次世代NEMブロックチェーン、Symbolに迫る(4) ~機能編 その2~

アグリゲートトランザクションとは?

アグリゲートトランザクションのアグリゲートは英語で集めるという意味があります。その名前の通り、このトランザクションは複数のトランザクションを1つ集めて処理するトランザクションとなります。

アグリゲートトランザクションの実行方法

  • コンプリート
  • コンプリート・オフライン
  • ボンデッド

上記3種類の実行方法について以下のストーリーで説明していきます。

Aliceはアポスティーユした申請データのハッシュ値をサービス提供者であるBobに提出したい。しかし、Aliceは手数料を所有していない。そのためBobが手数料を肩代わりしてAliceの提出を完成させる。

(追記) アグリゲートトランザクションは起案者のみに送信手数料の支払い義務があります。なので、Bobがアナウンスするアグリゲートトランザクションのために、Aliceへ手数料を渡しておく必要はありません。「ビジネスフロー的に手数料の必要な請求書のために費用をAliceに渡しておく」という意味合いでとらえてください。

論理プログラム

実装例を紹介する前に、簡略化したプログラムで要点だけを解説していきます。

コンプリート

aggTx.complete = [
    bob.transferTx{mosaics:[symbol.xym:1]} => alice,
    alice.transferTx{payload:"申請データハッシュ値"} => bob
]
signedTx = bob.sign(aggTx,[alice])
http.announce(signedTx)

アグリゲート・コンプリート・トランザクションはセキュリティトークン(STO)など、オペレータがAliceとBobの両方のアカウント(秘密鍵)を操作できる場合に使用できる方法です。まず、Aliceの申請トランザクションを パラメータに{payload:"申請データハッシュ値"}と指定したtransferTxで定義します。トランザクションの受領者は => bobと記述してBobへの送信とします。手数料を負担するBobのトランザクションはモザイクsymbol.xym:1を指定したtransferTxでAliceへと送信します。これらはaggTxという配列に格納しますが、残高の整合性を保つために手数料を負担するBobの送金を先に書きます。
トランザクションが定義できれば、Bobがオペレータとして署名します。Aliceは連署者として引数に指定します。最後にノードへHTTPへ署名データを通知してトランザクションを発行します。

コンプリート・オフライン


aggTx.complete = [
    bob.transferTx{mosaics:[symbol.xym:1]} => alice,
    alice.transferTx{payload:"申請データハッシュ値"} => bob
]
bobSignedTx = bob.sign(aggTx)
payload = bobSignedTx.payload

//オフライン
//Bobが署名した署名ペイロード(payload)をAliceに渡す。


aliceSignedTx = CosignatureTx.sign(alice, payload);
aliceSignature = aliceSignedTx.signature

//オフライン
//Aliceの署名(aliceSignature)をBobに返す

signedTx = signTransactionGivenSignatures(
    bob, 
    [CosignatureSignedTransaction(bobSignedTx.hash,aliceSignature,alice.publicKey)]
)
http.announce(signedTx)

アグリゲート・コンプリート・オフライン・トランザクションは、オペレータ(Bob)が連署人アカウントの秘密鍵を持っていない場合など、ノードへ通知する前に各連署者にオフラインで署名してもらったものを集めてからトランザクションを発行させる方法です。
Bobが署名したペイロードをAliceにオフライン(ここではブロックチェーンのネットワークを経由せず、という意味)で手渡して署名。AliceはBobに署名データだけを返してBobがAliceの署名データを添付して再署名します(signTransactionGivenSignatures)。最後にノードへ通知してトランザクションを発行します。

ボンデッド

aggTx.bonded= [
    bob.transferTx{mosaics:[symbol.xym:1]} => alice,
    alice.transferTx{payload:"申請データハッシュ値"} => bob
]

signedTx = bob.sign(aggTx)

//アグリゲートトランザクションをノード上にハッシュ値でロック
http.announce(
    bob.hashLockTx(signedTx)
)

//ロックされたハッシュをノードがら取得しaliceで署名したものをノードへ再通知
http.getTx(signedTx.hash).subscribe(aggTx =>
    cosignedTx = alice.signCosignatureTx(aggTx)
    http.announce(cosignedTx)
)

アグリゲート・ボンデッド・トランザクションは、連署が完成するまでノードにトランザクションを保留(ハッシュロック)しておき、求められた連署者による署名(signCosignatureTx)が完成するとブロックに取り込まれます。

実装例

実際に動くプログラムで見ていきましょう。

共通ロジック

3つのアグリゲートトランザクションで共通して記述するプログラムです。

スクリプト埋め込み
NODE = window.origin;
GENERATION_HASH = '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155';
EPOCH_ADJUSTMENT = 1616694977;

(script = document.createElement('script')).src = 'https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.1.js';
document.getElementsByTagName('head')[0].appendChild(script);

Google Chromeブラウザで適当にノードにアクセスして、F12をクリックしてコンソールに入力してください。例:http://ngl-dual-001.testnet.symboldev.network:3000/node/info

共通定義

//インポート
nem = require("/node_modules/symbol-sdk");
qr = require("/node_modules/symbol-qr-library");
hd = require("/node_modules/symbol-hd-wallets");

//アカウント作成
alice = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET);
bob = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET);

//インスタンス生成
nsHttp = new nem.NamespaceHttp(NODE);
txHttp  = new nem.TransactionHttp(NODE);
receiptHttp = new nem.ReceiptHttp(NODE);
accountHttp = new nem.AccountHttp(NODE);
transactionService = new nem.TransactionService(txHttp, receiptHttp);

//websocket準備
wsEndpoint = NODE.replace('http', 'ws') + "/ws";
listener = new nem.Listener(wsEndpoint,nsHttp,WebSocket);

//Alice申請トランザクション
tx = nem.TransferTransaction.create(
    nem.Deadline.create(),
    bob.address, 
    [],
    nem.PlainMessage.create('申請データハッシュ値'),
    nem.NetworkType.TEST_NET
);

//手数料負担トランザクション
feeTx = nem.TransferTransaction.create(
    nem.Deadline.create(),
    alice.address, 
    [nem.NetworkCurrencyPublic.createRelative(1)], //1symbol.xym
    nem.EmptyMessage,
    nem.NetworkType.TEST_NET
);

//アグリゲート
aggregateArray = [
    feeTx.toAggregate(bob.publicAccount),
    tx.toAggregate(alice.publicAccount),
]

コンプリート・トランザクション

準備:Bobへの入金
"http://faucet-0.10.0.x-01.symboldev.network/?recipient=" + bob.address.plain() +"&amount=2"

サービス提供者であるBobにあらかじめsymbol.xymを入金しておきます。

実行

//トランザクション作成
aggregateTx = nem.AggregateTransaction.createComplete(
    nem.Deadline.create(),
    aggregateArray,
    nem.NetworkType.TEST_NET,
    [],
    nem.UInt64.fromUint(1000000)
);

//トランザクション署名
signedTx = aggregateTx.signTransactionWithCosignatories(
    bob,
    [alice],
    GENERATION_HASH,
);

//ネットワークへの通知
listener.open().then(() => {
    transactionService.announce(signedTx,listener)
    .subscribe(x=>console.log(x))
});

//エクスプローラーで確認
console.log("http://explorer-0.10.0.x-01.symboldev.network/transactions/" + signedTx.hash);

コンプリート・オフライン

準備:Bobへの入金
"http://faucet-0.10.0.x-01.symboldev.network/?recipient=" + bob.address.plain() +"&amount=2"
実行

//トランザクション作成
aggregateTx = nem.AggregateTransaction.createComplete(
    nem.Deadline.create(),
    aggregateArray,
    nem.NetworkType.TEST_NET,
    [],
    nem.UInt64.fromUint(1000000)
);

//Bobでトランザクション署名
signedTx =  aggregateTx.signWith(bob,GENERATION_HASH);
signedHash = signedTx.hash;
signedPayload = signedTx.payload;
//Aliceで署名
aliceSignedTx = nem.CosignatureTransaction.signTransactionPayload(alice, signedPayload, GENERATION_HASH);
aliceSignedTxSignature = aliceSignedTx.signature;
aliceSignedTxSignerPublicKey = aliceSignedTx.signerPublicKey;
//BobがAliceの署名を添付
cosignSignedTxs = [
    new nem.CosignatureSignedTransaction(signedHash,aliceSignedTxSignature,aliceSignedTxSignerPublicKey)
];
recreatedTx = nem.TransactionMapping.createFromPayload(signedPayload);
signedTx = recreatedTx.signTransactionGivenSignatures(bob, cosignSignedTxs, GENERATION_HASH);

//ネットワークへ通知
listener.open().then(() => {
    transactionService.announce(signedTx,listener)
    .subscribe(x=>console.log(x))
});

//エクスプローラーで確認
console.log("http://explorer-0.10.0.x-01.symboldev.network/transactions/" + signedTx.hash);



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

準備:Bobへの入金
"http://faucet-0.10.0.x-01.symboldev.network/?recipient=" + bob.address.plain() +"&amount=20"

//トランザクション作成
aggregateTx = nem.AggregateTransaction.createBonded(
    nem.Deadline.create(),
    aggregateArray,
    nem.NetworkType.TEST_NET,
    [],
    nem.UInt64.fromUint(1000000)
);

//Bobで署名
signedAggregateTx = bob.sign(aggregateTx, GENERATION_HASH);

//ハッシュロックトランザクション作成
hashLockTx = nem.HashLockTransaction.create(
    nem.Deadline.create(),
    nem.NetworkCurrencyPublic.createRelative(10),
    nem.UInt64.fromUint(480),
    signedAggregateTx,
    nem.NetworkType.TEST_NET,
    nem.UInt64.fromUint(1000000)
);

//ハッシュロックトランザクションをBobで署名してネットワークへ通知
signedLockTx = bob.sign(hashLockTx, GENERATION_HASH);
listener.open().then(() => {
    transactionService.announceHashLockAggregateBonded(
      signedLockTx,
      signedAggregateTx,
      listener

    ).subscribe(aggTx => console.log(aggTx))
});


//部分署名中のトランザクションを取得
listener.open().then(() => {
    txHttp.getTransaction(signedAggregateTx.hash,nem.TransactionGroup.Partial)
    .subscribe(tx=>{
        //Aliceのトランザクションで署名
        cosignatureTx = nem.CosignatureTransaction.create(tx);
        signedTx = alice.signCosignatureTransaction(cosignatureTx);
        //エクスプローラーで確認
        console.log("http://explorer-0.10.0.x-01.symboldev.network/transactions/" + signedTx.parentHash);
        txHttp.announceAggregateBondedCosignature(signedTx)
        .subscribe(x=>console.log(x));
    })
});

以上、3種類のアグリゲートトランザクションを実行方法を紹介しました。
ぜひ、お試しください。