速習Symbolブロックチェーン8章、ロック編です。

URLとしてはこちら

それではやっていきます。

実践編

トランザクションを即時に実行する以外に、ロックするという機構があるようです。

凄いざっくり言うと、

・ハッシュロック

・シークレットロック

という2種類があるようです。

簡単に言えば、ハッシュロックは「これからトランザクションを発行するから、その予約をしておくよ」ということのようです。

なんでそんなの必要なの? という話なのですが、
アグリゲートトランザクション(複数の取引を1トランザクションにまとめたトランザクション)には2つありまして。

アグリゲートコンプリートと、アグリゲートボンデッドですね。

アグリゲートコンプリートはここまでで使用している、「必要な関係者すべての署名がそろったうえで」トランザクションを実行します。
「すべての署名がそろっている」ため、関係者はそこにまとめてある取引すべてに了承しているわけです。了承しているんだから、即時に取引してもいいよね? っていうのが、アグリゲートコンプリートです。

アグリゲートボンデッドはブロックチェーン上で「必要な関係者すべての署名」を揃えます。

必要な関係者すべての署名が集まらない場合、トランザクションは実行されません(有効期限は最大48時間とのことです)。

8.1 ハッシュロック

アグリゲートボンデッドはブロックチェーンを利用して署名を集めるため、無用に連発するとチェーンのリソースを食いますし、詐欺的なものもやりやすくなります。

ハッシュロックはアグリゲートボンデッドトランザクションを送信するための「供託金」らしいです。まぁ、実行するからには相手との了承済みだよね? っていう保証をさせるためなんでしょうね。

まぁそんな感じで。やっていきます。

// aliceのアカウントを作成する。
var alice = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(alicePrivateKey), sym.NetworkTypeEnum.testnet);

var bob = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(bobPrivateKey), sym.NetworkTypeEnum.testnet);

print(" alice address: ${alice.address}");
print(" bob address: ${bob.address}");

var tx1 = sym.TransferInfoV1.create(
  alice.publicAccount,
  bob.address,
  [
    sym.Mosaic(
      id: sym.NamespaceId('symbol.xym').getUnresolvedMosaicId(), 
      amount: sym.Amount(1000000))
  ]
  );

var tx2 = sym.TransferInfoV1.create(
  bob.publicAccount,
  alice.address,
  [],
  sym.PlainMessage.create('thank you!')
);

var deadline = sym.Deadline.createFromAdjustedValue(23362499068);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: 1);     // 手数料は100%。 必要な連署者1。

// トランザクションの通知。アグリゲートである必要があるらしい。         
var transactionSender = TransactionSender();

// Bobの連署のためにトランザクションハッシュを保持しておく。
_aggregateTransactionHash = await transactionSender.sendAggregateBonded(transSetting, [tx1, tx2]);

print('連署用のトランザクションハッシュ');
print(_aggregateTransactionHash.value);

内容としてはこんな感じです。

主なトランザクション内容としては、aliceがbobにモザイク送信、bobからは’thank you!’というメッセージを送ってもらうという形です。

実際のトランザクション送信は、transactionSender.sendAggregateBondedにて行っています。

sendAggregateBondedについてはこちら。

  /// トランザクションを送信する。
  /// AggregateBonded用
  /// 戻り値はAggregateBondedのトランザクションハッシュ。
  Future<Hash256> sendAggregateBonded(TransactionSetting transSetting, List<trans.TransactionInfo> transInfos) async {

    var availableHost = await getNetworkHost();

    var aggCompleteInfo = AggregateBondedInfoV2(transSetting.networkType, transInfos);

    var aggregateTransaction = await AggregateTransaction.create(transSetting, aggCompleteInfo);

    print("AggregateBondedTransaction");
    print("実行ペイロード");
    print(aggregateTransaction.payload);
    print("トランザクションハッシュ");
    print(aggregateTransaction.transactionHash.value);

    print("AggregateBondedは実行前にHashLockを行う必要がある");

    var hashLockTx = HashLockInfoV1.create(
      transSetting.signer.publicAccount,
      Mosaic(id: NamespaceId('symbol.xym').getUnresolvedMosaicId(), amount: Amount(10 * 1000000)),
      BlockDuration(480),
      aggregateTransaction.transactionHash
      );

    // BasicTransactionとして送信する。
    var hashLockTxHash = await sendTransaction(transSetting, hashLockTx);
//    return;

    print("別ノードでハッシュロックの伝播具合をチェックする");

{
    // このノードがまともに稼働しているかは不明だが、これを確認用にする。
    var availableHostX = await NetworkService.getHostInfo(useHost2);

    var result = await _validConfirmStatus(availableHostX.networkHost, hashLockTxHash);

    if (!result){
      print("エラーしているらしい。");
      return aggregateTransaction.transactionHash;
    }
}

    print("AggregateBondedTransactionをネットワークに通知する");
    var transRouteHttp = TransactionRoutesHttp(availableHost.networkHost);

    // トランザクションをノードに通知する。
    // var result = await transRouteHttp.announceNewTransaction(aggregateTransaction.payload.toUpperCase());
    var result = await transRouteHttp.announceAggregateBondedTransaction(aggregateTransaction.payload);

    print("トランザクションのアナウンスに成功した。");
    print(result.message);

    print("トランザクションの承認ステータスを確認する。");
    var isConfirm = await _validConfirmStatus(availableHost.networkHost, aggregateTransaction.transactionHash);

    return aggregateTransaction.transactionHash;

大まかな流れとしてはハッシュロックトランザクションを実行、承認後にアグリゲートボンデッドトランザクションを実行しています。

アグリゲートボンデッドトランザクションは先ほどの通り、ハッシュロックトランザクションを事前に実行しておく必要があります。

よってこのメソッド内にて、ハッシュロックトランザクションを実行しています。
※その後、必要があるかわからないですが、念のため別ノードに接続し、ハッシュロックトランザクションの伝播具合を確かめています(つもり)。
一応、注意点としてしばらく待ってからという話が書いてあったため。必要十分かはわかりません。

"AggregateBondedTransactionをネットワークに通知する"

上記のメッセージが表示されれば、ハッシュロックトランザクションの伝播が終わっていると考え、アグリゲートボンデッドトランザクションを送信します。

問い合わせの都合上、最終的にはアグリゲートボンデッドトランザクションのハッシュ値を返却しています。

このハッシュ値は連署用(として取得する必要があるため)用いることとします。

こんな形でアグリゲートボンデッドトランザクションを発行しました。

先ほどのアグリゲートボンデッドトランザクションはaliceが署名し、発行しています。

ただし、その内容にはbobに了承を行ってもらうトランザクションがあります。(Thank you!のメッセージ送信です)

bobのトランザクションを勝手にaliceが実行できても困るため、bobはbobでそのトランザクションに対し、署名して手続きを了承する必要があります。

下記がその内容です。


// bobのアカウントを作成する。
var bob = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(bobPrivateKey), sym.NetworkTypeEnum.testnet);

print("[連署者] bob address: ${bob.address}");

// 使用するネットワークホストを取得する。
var networkHostInfo = await TransactionSender.getNetworkHost();

var transHttp = await sym.TransactionRoutesHttp(networkHostInfo.networkHost);

var transInfo = await transHttp.getPartialInformation(_aggregateTransactionHash.value);

transInfo as sym.TransactionInfoDTO;

var aggTrans = transInfo.transaction;

aggTrans as sym.AggregateTransactionDTO;

print('署名対象ハッシュ');
print(transInfo.meta.hash.value);
var signature = await bob.sign(transInfo.meta.hash.serialize());

var cosignature = sym.DetachedCosignature(bob.publicKey, signature, transInfo.meta.hash);

var result = await transHttp.announceCosignature(cosignature);

print(result.baseMap.toString());

先ほどのトランザクションを取得するためのハッシュ値は_aggregateTransactionHashに保存し、共有してあります。

そのハッシュ値をtransactions/partial/{transactionId}に渡すことでトランザクション内容を取得することができます。

次にbobが署名する先です。

(ここまでで触れたか覚えていないのですが)aggregateTransactionで連署する場合、トランザクション自体に署名をするわけではなく、トランザクションのハッシュに署名すればいいとのことでした。恐らくそのほうが早い等あるんでしょうね。

さて、下記が実行した結果です。

連署の送信を終えました。

Explorerで確認すると、Aggregate inner transactionsとして2件あるのを確認できます。

Confirmationもconfirmedのため、無事に承認されていますね。

念のためウォレットでも確認しますが。

少なくともaliceに向けて”thank you!”と来ているのが確認できます。

8.2 シークレットロック・シークレットプルーフ

速習Symbolによるとシークレットロック、シークレットプルーフというのがあるようです。

シークレットロックはパスワード付きで指定モザイク(トークン)をロックするトランザクション。

シークレットプルーフはシークレットロックでロックされたモザイク(トークン)を受け取るためのトランザクションのようです。

  • aliceがbobに100XYMあげる。
  • 交換にbobがaliceに100XOMあげる。
  • 100XYMと100XOMのため、別チェーンの交換になる。
    (そのため、アグリゲートボンデッドでは不可能である。
  • 100XYMと100XOMの送信をそれぞれ、シークレットロックする。
  • それぞれで送信が担保されていることが確認できる。
  • シークレットロックの鍵を同時に交換し、それぞれがシークレットプルーフトランザクションを実行することで、モザイク(トークン)を受け取ることができる。

みたいな用途と理解しました。

シークレットロック

ということで、シークレットロックです。

先ほどのことを実現するため、必要なものを準備します。

  • モザイク送信先のアドレス
  • 送信モザイク(トークン)
  • ロック用キーワード。
  • 解除用キーワード。
  • ロック作成に使用したアルゴリズム。

それらを先に用意し、シークレットロックトランザクションを実行してトークンをロックします。

その後、モザイク受信側にがシークレットプルーフトランザクションを実行し、モザイクの受信が行える。

ただし、シークレットロックトランザクションの内容は受信側から見ることはできるが、上記の中で”解除用キーワード”のみ不明になります。
よって、解除用キーワードのみ別の方法で渡す、という感じです。

速習Symbolの通りにやっていきます。

ざっくりとaliceがbobにシークレットロックトランザクションで1XYMを贈ります。

ランダムの値を生成し、ロック用キーワード(_secret)、解除用キーワード(_proof)にします。

これをシークレットロックトランザクションとして実行します。

シークレットロックトランザクションには当然に解除用キーワードを指定せず、ロック用キーワードとそれを生成するためのアルゴリズムを指定します。

// 必要アカウントを作成する。
var alice = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(alicePrivateKey), sym.NetworkTypeEnum.testnet);

var bob = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(bobPrivateKey), sym.NetworkTypeEnum.testnet);

var rnd = encrypt.SecureRandom(20);

// _secretは解除
_secret = sym.Hash256.byteToHash(rnd.bytes);      // ロック用キーワード。
_proof = hex.encode(rnd.bytes);                   // 解除用キーワード。

print("ロック用KW: ${_secret.value}");
print("解除用KW:   $_proof");

// 送信用モザイク。
var mosaic = sym.Mosaic(
      id: sym.NamespaceId('symbol.xym').getUnresolvedMosaicId(), 
      amount: sym.Amount(1000000));

// 有効期限はブロック数換算。365*2880が最大値?
var duration = sym.BlockDuration(120);

// シークレットトランザクション。
var secretLockTx = sym.SecretLockInfoV1.create(
  alice.publicAccount,
  bob.address,
  _secret,
  mosaic,
  duration,
  sym.LockHashAlgorithmEnum.sha3_256);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100));  

// トランザクションの通知。
var transactionSender = TransactionSender();

await transactionSender.sendSecretLockTransaction(transSetting, secretLockTx, _secret);

なお、transactionSender.sendSecretLockTransactionとトランザクション実行メソッドを別に用意しました。

これはシークレットトランザクションの内容を確認するのはTransactionsRouteのAPIではなく、SecretLockRouteであるlock/secret/を使用して取得するためです。(たぶん、これであってる?)

ここまでのトランザクション送信メソッドとあまり変わり映えしませんが、下記のようにしています。

  Future<Hash256> sendSecretLockTransaction(TransactionSetting transSetting, trans.TransactionInfo transInfo, Hash256 secret) async {

    var basicTransaction = await BasicTransaction.create(transSetting, transInfo);

    print("実行ペイロード");
    print(basicTransaction.payload);
    print("トランザクションハッシュ");
    print(basicTransaction.transactionHash.value);
//    return basicTransaction.transactionHash;
    var availableHost = await getNetworkHost();

    var transRouteHttp = TransactionRoutesHttp(availableHost.networkHost);

    // トランザクションをノードに通知する。
    var result = await transRouteHttp.announceNewTransaction(basicTransaction.payload);

    print("トランザクションのアナウンスに成功した。");
    print(result.message);

    print("トランザクションの承認ステータスを確認する。");
    var transStateRouteHttp = TransactionStateRoutesHttp(availableHost.networkHost);

    var processing = true;

    while(processing){
      await Future.delayed(const Duration(seconds: 1));

      // 結果の確認を行う。
      var resultState = await transStateRouteHttp.getStatus(basicTransaction.transactionHash);

      // 未認証の場合のみ、終わるまで継続。
      switch (resultState.group){
        case TransactionGroupEnum.confirmed:
          print("トランザクションが承認されました。やったぜ!");
          processing = false;
          break;
        case TransactionGroupEnum.unconfirmed:
          print("トランザクションは未承認です。");
          break;
        case TransactionGroupEnum.failed:
          print("トランザクションにエラーがありました。");
          if (resultState.code != null) print(resultState.code!.value);
          return basicTransaction.transactionHash;
        default:
          print("パーシャル!!! パーシャル……?");
          return basicTransaction.transactionHash;
      }
    }

    print("承認後はSecretLockRoutesから結果を取得する。");

    var secretRouteHttp = SecretLockRoutesHttp(availableHost.networkHost);

    var criteria = SearchSecretLockEntriesCriteria();

    criteria.secret = Secret(secret.value);

    var secretInfo = await secretRouteHttp.searchEntries(criteria);

    print("取得結果。");
    print(jsonEncode(secretInfo.baseMap));

    return basicTransaction.transactionHash;

  }

実行します。

トランザクションが承認されました。

ロック用キーワード(ロック用KW)、解除用キーワード(解除用KW)が表示されています。

最後の取得結果から

{
	"data": [
		{
			"lock": {
				"version": 1,
				"ownerAddress": "9808ACFBB0A6E7C3AB692111A93807C35F80D33F0BC5CC00",
				"mosaicId": "72C0212E67A08BCE",
				"amount": "1000000",
				"endHeight": "797903",
				"status": 0,
				"hashAlgorithm": 0,
				"secret": "5C1D0E4F65C475CA4453F3696A73A7E37ECC4CC4406A6BE8D5A436AD15071E7A",
				"recipientAddress": "9873F010CF581FAC0AAF94BC24C75F255D0FB380BD160147",
				"compositeHash": "443B1559126F0F672A4E7752818DC61976FB1EF68BC45BB56BE4896D58F252AF"
			},
			"id": "64FC24C9E15C6DE0D69FEC52"
		}
	],
	"pagination": {
		"pageNumber": 1,
		"pageSize": 20
	}
}

オーナーアドレス(ownerAddress)、モザイクiD(mosaicId)、数量(amount)、終了ブロック(endHeight)、ハッシュアルゴリズム(hashAlgorithm)、ロック用キーワード(secret)、送信先アドレス(recipientAddress)を確認することができます。

シークレットプルーフ

bobが先ほどのシークレットロックを解除していきます。

bobはすでに解除用キーワードを知っているテイとします。

 // 必要アカウントを作成する。
var bob = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(bobPrivateKey), sym.NetworkTypeEnum.testnet);

// トランザクション生成。
// 変換が見えているのでうまくない。サイズもここで計算しているが、内部でやるようにいつか直す。
var proofTx = sym.SecretProofInfoV1.create(
  bob.publicAccount, 
  bob.address, _secret, (_proof.length / 2).toInt(), sym.LockHashAlgorithmEnum.sha3_256, 
  Uint8List.fromList(hex.decode(_proof)));

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: bob, 
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100));  

// トランザクションの通知。アグリゲートである必要があるらしい。         
var transactionSender = TransactionSender();

await transactionSender.sendTransaction(transSetting, proofTx);

途中で変なコメントがありますが、まぁね。いずれ修正する予定です。
ここでは面倒だったため変更しません。

実行します。

雰囲気的に通りました。

※シークレットロックトランザクションをSecretLockRouteのAPIから取得した際に、「誰が誰に何を送信するのか?」が取得されましたが、このトランザクション自体の結果からは読み取ることはできないらしいですね。

そんなわけで、別途ReceiptのAPIに問い合わせればわかる、とのことでした。

条件がbobであり、lockSecretCompletedということでシークレットプルーフトランザクションが成功したもののみ取れるように実行しました。

// bobのアカウントを作成する。
var bob = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(bobPrivateKey), sym.NetworkTypeEnum.testnet);

print("[送信元] bob address: ${bob.address}");

var networkHostInfo = await TransactionSender.getNetworkHost();
var receiptHttp = sym.ReceiptRoutesHttp(networkHostInfo.networkHost);

var criteria = sym.SearchTransactionStatementsCriteria();
criteria.targetAddress = bob.address;
criteria.receiptType.add(sym.ReceiptTypeEnum.lockSecretCompleted);

var result = await receiptHttp.search(criteria);

print(result.baseMap);

結果としては下記。

なんかたくさんあるのはテストのため、数度にわたって実行したからです。

ブロック高(height)が最大である、height: 797798のみを下記に抜き出しました。

{statement: {height: 797798, source: {primaryId: 1, secondaryId: 0}, receipts: [{version: 1, type: 8786, targetAddress: 9873F010CF581FAC0AAF94BC24C75F255D0FB380BD160147, mosaicId: 72C0212E67A08BCE, amount: 1000000}]}, id: 64FC26893BE93F2BD417AB05, meta: {timestamp: 26996068334}}

実行されたブロック高(height)、モザイクId(mosaicId)、数量(amount)などが読み取れます。

感想編

時間がかかりました。

通常のトランザクションでは送信するペイロードサイズから手数料を計算するのですが、アグリゲートボンデッドトランザクションでは送信するペイロードサイズ+連署分のサイズを考慮して計算する必要があるようでした。

また、連署の送信ですが、ベーシックトランザクションのようにペイロードを組み立ててトランザクションを送信するのかと思っていたのですが、違うようですね。

トランザクションタイプを見てあれ? 存在しない? と少々困惑しました。

結果的にTransaction routesのAPIにjson形式でputするということのようですね。

アグリゲートボンデッドトランザクションは凄いな、と再認識しました。

現場で使えるヒントのタイマー送信なのですが、いまいちよく理解できなかったです。

bobが宿泊業を営んでおり、aliceがお客さんとして予約しようとしていました。

bobがアグリゲートボンデッドトランザクションで

  • bobがaliceにシークレットロックトランザクションで50XYM(宿泊料)送信
  • aliceがbobに50XYM(宿泊料)送信

を行ったとする。

aliceが署名することで、alice → bobの50XYMが送金される。

当日チェックインした場合、特にすることはない。
(bobのシークレットロックトランザクションは送信されず、期限を超えたため、bobの元に戻る。
足し引きの結果、aliceがbobに50XYM送金しただけで終わる)

事前にキャンセルとなった場合、シークレットプルーフトランザクションを実行する。
(bobがaliceに50XYM送信するため、結果的にaliceもbobも最初と同じ数量になる)

ということなんでしょうか?
チェーン外のことを気にすると、税金とか影響どうなんだ? という思いがなくはないですが。。。

投稿者 和泉