速習Symbolブロックチェーン12章、オフライン署名編です。

URLとしてはこちら。

アグリゲートトランザクションでは、自分とは違うアカウントのトランザクションを代理で作ることができました。

その代理で作られたトランザクションが実行されるには、関連するアカウントの署名を集める必要があります。

関連するアカウントの秘密鍵を持っている場合、署名し、アグリゲートコンプリートトランザクションを実行することができました。
関連するアカウントの秘密鍵を持っていない場合、アグリゲートボンデッドトランザクションを実行し、チェーンを介してそれぞれのアカウントに署名をしてもらうことができました。

オフライン署名ではペイロードを共有することで署名してもらい、その署名を用いてアグリゲートコンプリートトランザクションを実行するということのようです。
やり取りするデータは署名とペイロードのみであり、アグリゲートコンプリートトランザクションであることから、すべての署名が集まらない限り実行されないものであるようです。


実践編

12.1 トランザクション作成から、12.3 aliceによるアナウンスまで。

途中で切り離すところがなかったため、そのまま一連の流れで実行します。

全容としては、

  1. aliceがアグリゲートコンプリートトランザクションを作成する。
    アグリゲートトランザクションの内容は
    [aliceがbobに”tx1″とメッセージを送信する][bobがaliceに”tx2″とメッセージを送信する]
    aliceがトランザクションを作成するも、bobにメッセージの送信をしてもらうため、bobの連署が必要になる。
  2. aliceがbobにトランザクションのペイロードを渡す。
    bobはペイロードから「aliceがどんなトランザクションを作成したのか?」を確認することができる。
    bobがaliceの作成したトランザクションを承認する場合、bobが連署を行い、連署内容をaliceに返却する。
  3. aliceはbobが承認した証として連署を受け取る。
    作成したトランザクションにbobの連署を組み込み、アグリゲートコンプリートトランザクションとして、ネットワークに通知する。

という流れです。

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

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

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

print('12.1 トランザクション作成');

// aliceとbobで相互にメッセージを送りあう。
var tx1 = TransferTxInfo.create(alice.publicAccount, bob.address, [], PlainMessage.create('tx1'));
var tx2 = TransferTxInfo.create(bob.publicAccount, alice.address, [], PlainMessage.create('tx2'));

// Transaction設定。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: generationHashSeed, 
  networkType: NetworkType.testnet, 
  deadline: Deadline.create(epochAdjustment),
  fee:FeeMultiplier(100),   // 手数料は100%。
  cosignatoriesQ: 1);       // 連署は1(alice起案、bobの署名も必要なため1)

// 複数トランザクションをAggegateCompleteとしてまとめる。Txの入れ子。
var aggCompleteTxInfo = AggregateCompleteTxInfo.create(transSetting.networkType, [tx1, tx2]);

// AggregateTransactionを生成する。(署名済みペイロードの生成を行う)
var aggTx = await AggregateTransaction.create(transSetting, aggCompleteTxInfo);

// PayloadはStringであることを示す。
String txPayload = aggTx.payload;

// 署名済みペイロードを出力する。
print('signedPayload: $txPayload');

print('--------------------');
print('12.2 Bobによる連署');

// signedPayloadからトランザクションを復元する。
var deserializedTx = DeserializedTransaction.deserialize(txPayload, generationHashSeed);

// Mappingなんて便利なものはないため、ちまちまと出力することとする。
var desTx = deserializedTx.transaction;
desTx as AggregateTransaction;

// トランザクション設定(デシリアライズ)を取得する。
var desTxSetting = deserializedTx.deserializedSetting;

{
  print('適当にデシリアライズした値を出力する。');
  print('AggregateCompleteTransactionか?: ${desTx.txInfo is AggregateCompleteTxInfo ? 'yes' : 'no' }');
  print('トランザクションサイズ: ${desTx.getTransactionSize()}');
  print('Tx fee: ${desTxSetting.fee.value.toInt()}');
  print('署名者: ${desTxSetting.signer.address}');
  print('NetworkType: ${desTxSetting.networkType.value}');
  print('innerTxの出力');
  for (var innerTx in desTx.txInfo.transactions){

    print('transactionType: ${innerTx.transactionType.value}');
    // ※ここではすでにTransferとわかっているため、Transferとして扱う。
    innerTx as TransferTxInfo;
    print('送信元アドレス: ${innerTx.publicAccount.address.value}');
    print('送信先アドレス: ${innerTx.recipientAddress.value}');
    print('メッセージ: ${innerTx.message.message}');
  }
}

print('署名が正しいか確認する');
{
  var signingBytes = deserializedTx.getSigningBytes(generationHashSeed);

  // 検証状態を取得する。
  var res = await alice.publicAccount.verifyByte(signingBytes, desTxSetting.signature);

  print('aliceが署名したペイロードである: ${res ? 'yes' : 'no'}');
}

print('bobによる署名を行う');
// なお、bobは別の環境という想定? の上であるため、トランザクションに直接署名を追加しない。
var bobCosignature = await bob.cosignatureWithHash256(deserializedTx.transaction.txHash);

String bobCosignaturePayload = bobCosignature.serialize();

print('bobの署名クラスの中身(Payload): $bobCosignaturePayload');

print('--------------------');
print('12.3 Aliceによるアナウンス');

print('bobの署名がaliceに渡されたたため、aliceがトランザクションをノードに通知する');

// ここまででaliceの生成したTx、bobがpayloadより復元したTxがある。
// aliceがTxを取っておいてあればbobの連署を追加することでTxが完成する。 x1
// bobが復元したTxに連署を追加することでTxが完成する。 x2
// x1とx2のペイロードが同値となる。これで必要な署名がそろっているため、誰でもTxを通知することができる。
// alice、bob、なんであれば第三者でも。
{
  // bobの連署を復元する。
  var reduceCosignature = Cosignature.deserialize(bobCosignaturePayload);

  // aliceがTxを完成させる。 x1
  aggTx.addCosignatories([reduceCosignature]);

  // bobがTxを完成させる。
  desTx.addCosignatories([reduceCosignature]);

  print('aliceとbobのTxは一致${aggTx.payload == desTx.payload ? 'した' : 'しない'}');

}

// txが一致している限り、誰がアナウンスしてもよい。
// aliceが送信する。
var transactionSender = TransactionSender();

transactionSender.sendSignedTransaction(aggTx);

長いですね。

“12.1 トランザクション作成”ではトランザクションを作成し、最後にペイロードを出力しています。

“12.2 Bobによる連署”ではペイロードからトランザクションを復元し、bobが連署を行います。
そして最終的には連署内容をシリアライズし、aliceに返却するイメージです。

“12.3 Aliceによるアナウンス”ではaliceが受け取った連署をからトランザクションを完成させます。同様にbobはペイロードから復元したトランザクションに連署を追加して完成させます。
aliceの手元にあるトランザクションデータとbobの手元にあるトランザクションデータのペイロードを比較し、同値であることを比較します。
(同値であれば、シリアライズ・デシリアライズと連署の追加が同じ結果をもたらす、という証明のためです)

ペイロードはalice、bobどちらも同じものがあり、それは完成した状態であるため、アナウンスは誰でも可能です。
ここではaliceが実行します。

以上です。下記が実行結果です。

12.1 トランザクション作成

12.2 Bobによる連署

12.3 Aliceによるアナウンス

トランザクションの実行結果

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

9.6 現場で使えるヒント

マーケットプレイスレス

ハッシュロックが必要ではないため、ハッシュロック費用が掛からず、取引を完了することができるようです。

ただし、ただ渡されたハッシュデータに署名をするだけでは危ないというようです。

ハッシュに署名をするだけではその取引内容がわからないため、ちゃんとペイロードから「何を行うものなのか?」を確認し、
そこからハッシュ値を作成し、署名を行おうね、ということのようですね。
仮に署名要求されているハッシュ値と、ペイロードから生成されるハッシュ値が違う場合、それはペイロードとは違う取引にサインしろ、という要求であるということですね。


感想編

デシリアライズというか、ペイロードからトランザクションデータを生成することを考えていませんでした。
少し前の監視と同じですね。慌てて作った的な。

それを行う過程でちょこちょことコードの修正を行ったため、それなりに時間がかかりました。ただ、もとよりいくらか作りがスリムになった? ような? 予感だけします。

オフライン署名を使うとハッシュロックトランザクションの費用がかからない&秘密鍵の管理はそれぞれが行えるという利点でしょうか。

  1. 会社内にペイロードと署名を管理するシステムを用意する。
  2. 秘密鍵はそれぞれの従業員で管理する。
  3. 従業員Aが何かの提案をアグリゲートトランザクション化し、システムに登録する。
  4. システムにペイロード(データ)が登録され、それぞれに署名を求められる。
  5. それぞれがペイロードを取得し、内容の確認。承認する場合、システムに署名を登録する。
  6. システムが署名を集まったのを確認し、ブロックチェーンに通知する。

ただこれだけで署名と実行の証明ができそうです。
※プライベートチェーンでやればいいんじゃ? という話もあるかもしれませんが。

以上です。お疲れさまでした。残るはあと1本ですね。

投稿者 和泉