9. マルチシグ化

速習Symbolブロックチェーン9章、マルチシグ化編です。

URLとしてはこちら

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

実践編

注意事項に1つのマルチシグアカウントに登録できる連署者の数は25となります。つまり、1つの子アカウントに対し、親になれるのは25ということなんでしょう。

また、最大3階層までとのことです。

この値については、apiノードの/network/propertiesにアクセスすると、確認できます。

こういう値はおそらく、ハードフォークすることで変更できる値なんでしょう。

9.0 アカウントの準備

マルチシグを構成するためのアカウントを用意します。

var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);

var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);
var carol3 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol3PrivateKey), sym.NetworkTypeEnum.testnet);
var carol4 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol4PrivateKey), sym.NetworkTypeEnum.testnet);
var carol5 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol5PrivateKey), sym.NetworkTypeEnum.testnet);

print("takashi: ${takashi.address.toString()}");
print("carol1: ${carol1.address.toString()}");
print("carol2: ${carol2.address.toString()}");
print("carol3: ${carol3.address.toString()}");
print("carol4: ${carol4.address.toString()}");
print("carol5: ${carol5.address.toString()}");

こんな感じで。速習Symbolではbobとcarol1-5でやっていました。

秘密鍵をランダムでやると(1日で終わるとは限らない僕のペースでは)困ってしまうので、定数化しています。

過去の分も似たような形でやっていたため、名前の重複すると(自分で)混乱する可能性が高いため、bobの代わりにtakashiとしました。よろしくtakashi。

9.1 マルチシグの登録

説明の通り、Symbolのマルチシグアカウントは新規で生まれるわけではなく、既存のアカウントを利用します。つまり、先ほどのtakashiをそのままマルチシグにするような感じです。

var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);

var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);          
var carol3 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol3PrivateKey), sym.NetworkTypeEnum.testnet);          
var carol4 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol4PrivateKey), sym.NetworkTypeEnum.testnet);          

// マルチシグの追加アカウントたち。
var cosigners = [carol1, carol2, carol3, carol4];

// マルチシグの追加アカウントたち(アドレス)
var cosignersAddr = List.generate(cosigners.length, (index) => cosigners[index].address);

// 速習では承認/除名で必要な署名者数が同数となっているが、値の判別のため、それぞれを別とする。
// また、(元SDKの引数順を見ていないが)速習では承認、除名という引数順であった。
// 理由は不明だが自作では除名、承認となっている。
var multisigTx = sym.MultisigAccountModificationInfoV1.create(
  takashi.publicAccount,
  Int8(2),    // 除名のために必要な最小署名者数増分
  Int8(3),    // 承認のために必要な最小署名者数増分
  cosignersAddr,  // 追加アドレス。
  []
);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: takashi, // マルチシグ化したいアカウント(発行者)
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: cosigners.length);     // 手数料は100%。

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

await transactionSender.sendAggregateComplete(transSetting, [multisigTx], cosigners);

MultisigAccountModificationInfoV1を作っているところで、ちょっと変更している部分はありますが、速習と同じ感じかなと思います。

なお、アグリゲートコンプリートトランザクションで実行しています。

長くなってしまいましたが承認されました。

9.2 確認

takashiがマルチシグ化されているのか? を確認するようです。

同様にcarol1から見て、takashiが子となっているかを確認します。

// アドレスを取得するため、takashiとcarol1を生成する。
var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);

var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);


// 使用するネットワークホストの取得と、RestAPIのアクセス用クラスをインスタンス化。
var networkHostInfo = await TransactionSender.getNetworkHost();

// マルチシグ情報を取得する。
var httpMultisig = sym.MultisigRoutesHttp(networkHostInfo.networkHost);


print("マルチシグ化したアカウントの確認(takashi)");

print((await httpMultisig.getMultisigInformation(takashi.address)).baseMap.toString());

print("連署者アカウントの確認");

print((await httpMultisig.getMultisigInformation(carol1.address)).baseMap.toString());

マルチシグ化したアカウントの確認(takashi)

takashi側でマルチシグ化されているのが確認できます。

minApprovalが3、minRemovalが2と、先ほどの設定どおりです。

cosignatoryAddressesにはcarol1-4が(おそらく)並んでいます。

multisigAddressesは自分が親となる、子どものアドレスが設定されるようです。takashiは誰の親でもないため、空となっているのが確認できます。

連署者アカウントの確認

こちらはcarol1で確認しています。こちらはマルチシグアカウントではないため、minApprovalおよびminRemovalは0となっています。同様に親が存在しないため、cosignatoryAddressesは空になります。

子どもとなるtakashiのアドレスがmultisigAddressesに登録されています。

9.3 マルチシグ署名

マルチシグ化されたアカウントは自身でやるのではなく、親アカウントが操作する必要があります。

そのため、この先の例ではtakashiのアカウントから送信するが、実際にそのトリガとしてアクションを起こすのはcarol1となっています。

アグリゲートコンプリートトランザクションで送信

アグリゲートコンプリートトランザクションで実行するコードは以下。

// 必要アカウントを作成する。
var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);
var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);
var carol3 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol3PrivateKey), sym.NetworkTypeEnum.testnet);

// carol1は起案者になるため、連署はcarol2、carol3のみ。
// (takashiのminApprovalが3のため、3名が署名すれば、連署は成立する)
var cosigners = [carol2, carol3];

// aliceが送信を受ける。
var alice = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(alicePrivateKey), sym.NetworkTypeEnum.testnet);

// 転送トランザクション作成。
var transTx = sym.TransferInfoV1.create(
  takashi.publicAccount,      // 実行する(送信元)アカウントをtakashiとする。
  alice.address,
  [sym.Mosaic(id: sym.NamespaceId("symbol.xym").getUnresolvedMosaicId(), amount: sym.Amount(1000000))],
  sym.PlainMessage.create('konichiha!!'),
);

// Transaction設定を行う。
// 署名者はcarol1。takashiのアカウントから送信するが、takashiは子のため、自身で起案を行わない。
var transSetting = sym.TransactionSetting(
  signer: carol1,
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: cosigners.length);     // 手数料は100%。

// トランザクションの通知。
print('アグリゲートコンプリートトランザクションで送信');
var transactionSender = TransactionSender();

await transactionSender.sendAggregateComplete(transSetting, [transTx], cosigners);

僕の作り上、 transactionSender.sendAggregateComplete()でアグリゲートコンプリートトランザクション実行後、承認を待って承認内容を出力しています。

アグリゲートトランザクション内にて、実際の転送トランザクション部分は以下。

ウォレットで確認すると下記の形。

takashiのアドレスは末尾HDY。carol1のアドレスは末尾JBI。

署名者(Signer起案者)はcarol1となっているが、トランザクション詳細の送信者は末尾HDYのtakashiとなっています。

takashiの署名は関係なく、carol1がtakashiのトークンを転送できたことが確認できます。

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

takashiの親アカウントの連署を集めるのに、全員がトランザクションに即時署名ができるとは限らないため、アグリゲートボンデッドトランザクションでも実行可能となっています。

ちょっと冗長ではありますが、takashiがaliceに転送トランザクションを実行するアグリゲートボンデッドトランザクションをcarol1が起案として実行します。

なお、過去のアグリゲートボンデッドトランザクションの連署と同じく、トランザクションハッシュをチェーンから取得してきて、carol2、carol3が連署するようにします。

アグリゲートボンデッドトランザクションを通知してから、30秒待機しているので、ちょっと長いですね。

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

// aliceが送信を受ける。
var alice = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(alicePrivateKey), sym.NetworkTypeEnum.testnet);

// 転送トランザクション作成。
var transTx = sym.TransferInfoV1.create(
  takashi.publicAccount,      // 実行する(送信元)アカウントをtakashiとする。
  alice.address,
  [sym.Mosaic(id: sym.NamespaceId("symbol.xym").getUnresolvedMosaicId(), amount: sym.Amount(1000000))],
  sym.PlainMessage.create('zuttomo dayo!!'),
);

// Transaction設定を行う。
// 署名者はcarol1。carol1以外の連署者が署名すればよいため、連署者数はcarol2,carol3の2が必要。
var transSetting = sym.TransactionSetting(
  signer: carol1,
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: 2);     // 手数料は100%。

// トランザクションの通知。
print('アグリゲートボンデッドトランザクションで送信');
var transactionSender = TransactionSender();

var transHash = await transactionSender.sendAggregateBonded(transSetting, [transTx]);


// ここから連署を行う。
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);
var carol3 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol3PrivateKey), sym.NetworkTypeEnum.testnet);

var cosigners = [carol2, carol3];

var networkHostInfo = await TransactionSender.getNetworkHost();

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

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

transInfo as sym.TransactionInfoDTO;

var aggTrans = transInfo.transaction;

aggTrans as sym.AggregateTransactionDTO;

print('署名対象ハッシュ');
print(transInfo.meta.hash.value);

print("連署を行う");
for (var element in cosigners) {
  
  var signature = await element.sign(transInfo.meta.hash.serialize());

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

  await transHttp.announceCosignature(cosignature);
}

実行結果は以下の通り。

連署関係は過去の章と同じ処理で。

ウォレットで確認すると同様にトランザクションが承認されていることが確認できました。

9.5 マルチシグ構成変更

現在、takashiは承認のために3アカウント、除名には2アカウントとなっています。

この数値をいじれたり、また親アカウントの構成を変えることができるようです。

マルチシグ構成の縮小。

除名および承認に必要な連署者数を-1とするようです。

また、carol3についてもマルチシグ構成から除名するようです。

// 必要アカウントを作成する。
var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);
var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);
var carol3 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol3PrivateKey), sym.NetworkTypeEnum.testnet);

// carol1は起案者になるため、連署はcarol2、carol3のみ。
// (takashiのminApprovalが3のため、3名が署名すれば、連署は成立する)
var cosigners = [carol2, carol3];

// マルチシグアカウント情報変更設定を行う。
// takashiの必要な最小署名者を-1にし、carol3を除名する。
var multisigTx = sym.MultisigAccountModificationInfoV1.create(
  takashi.publicAccount, 
  Int8(-1),   // 除名のために必要な最小署名者数増分
  Int8(-1),   // 承認のために必要な最小署名者数増分
  [], 
  [carol3.address]);

// Transaction設定を行う。
// 署名者はcarol1。takashiのアカウントから送信するが、takashiは子のため、自身で起案を行わない。
var transSetting = sym.TransactionSetting(
  signer: carol1,
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: cosigners.length);     // 手数料は100%。

// トランザクションの通知。
print('アグリゲートコンプリートトランザクションで送信');
var transactionSender = TransactionSender();

await transactionSender.sendAggregateComplete(transSetting, [multisigTx], cosigners);

完了した。

9.2 確認で行った動作を再度実行します。

takashiのminApprovalが2、minRemovalが1となっているのが確認できます。

また、cosignatoryAddressesもcarol3が除名されたことで、3件となっていることが確認できます。

これでtakashiのマルチシグ構成を減らすことを確認しました。

連署者構成の差し替え

連署者構成の差し替えを行います。

現在、takashiの親アカウントはcarol1、carol2となっています。

これをcarol2をout、carol5をinとしていきます。minApprovalおよびminRemovalについては変動させません。


// 必要アカウントを作成する。
var takashi = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(takashiPrivateKey), sym.NetworkTypeEnum.testnet);
var carol1 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol1PrivateKey), sym.NetworkTypeEnum.testnet);
var carol2 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol2PrivateKey), sym.NetworkTypeEnum.testnet);
var carol5 = await sym.Account.createFromPrivateKey(
  sym.PrivateKey(carol5PrivateKey), sym.NetworkTypeEnum.testnet);

// carol1は起案者になるため、連署はcarol2のみ。
// (takashiのminApprovalが2のため、2名が署名すれば、連署は成立する)
// ただしcarol5を追加するため、carol5の連署も必要となる。
var cosigners = [carol2, carol5];

// マルチシグアカウント情報変更設定を行う。
// 署名者数増分を0にして、変更なし。
// carol2を削除し、carol5を設定する。
var multisigTx = sym.MultisigAccountModificationInfoV1.create(
  takashi.publicAccount, 
  Int8(0),   // 除名のために必要な最小署名者数増分
  Int8(0),   // 承認のために必要な最小署名者数増分
  [carol5.address], 
  [carol2.address]);

// Transaction設定を行う。
// 署名者はcarol1。takashiのアカウントから送信するが、takashiは子のため、自身で起案を行わない。
var transSetting = sym.TransactionSetting(
  signer: carol1,
  generationHash: generationHashSeed, 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100),
  cosignatoriesQ: cosigners.length);     // 手数料は100%。

// トランザクションの通知。
print('アグリゲートコンプリートトランザクションで送信');
var transactionSender = TransactionSender();

await transactionSender.sendAggregateComplete(transSetting, [multisigTx], cosigners);

とりあえず実行されました。

再度、9.2の確認を実行します。

takashiのminApprovalが2、minRemovalが1で変化はありません。

cosignatoryAddressesに3名が並んでいますが、この状態では冒頭のアドレスとは一致しないため、わかりづらいですね。。。

ということで、エンコード前アドレスも出力してきました。

アッパーにしてないのであれですが、carol1は9812…f485、carol4が987c…991e、carol5が9800…3257で親アドレスになっていることが確認できます。

感想編

現場で使えるヒントもこちらで。

多要素認証

ちゃんとやればセキュリティは安全に。ただ、変にやると漏れてしまうねっていうところでしょうか。

複数人で1アカウントの運用もできてしまうので便利ではありますが、一度ミスをした人がいれば全体にも波及する恐れがありますね。

MAGIシステムじゃないですが、複数人の承認がないと実行されないのは面白いですよね。
別に複数人でなくても、例えば金庫用のアカウントなんかがある場合は、金庫用のアカウントの親アカウントを2つ用意し、それぞれを普段使いのデスクトップPC、スマホと導入しておけば、片方だけでは送金とかされない安全性はありますよね。
……故障などで取り出せなくなることもあるため、秘密鍵の管理は別途、大事になるところだと思いますが。。。

アカウントの所有

親アカウントを所持していることで、子アカウントのアカウントも(それが発行したモザイクなども)所持しているということですね。親アカウントの構成を自由に組み替えられるため、誰かに子アカウントの譲渡もできる、という感じでしょうか。

秘密鍵は無効(というより、子アカウントでのトランザクション発行が無効なのかな?)とありました。
ふと思ったのは、子アカウントに向けて暗号化メッセージを送信した場合、メッセージの復号には子アカウントの秘密鍵が必要になるのでは? と思いました。

これは言い換えると、子アカウントに対しては暗号化メッセージを送らないほうがよさそうな気もしますね。もちろん運用次第ですけど。
子アカウントを譲渡した際に子アカウントの秘密鍵の譲渡がされない場合、子アカウントへの暗号メッセージは譲渡された側が確認することができませんし、子アカウントの元の持ち主が見ることが可能かなと思います。そうだとしたらちょっと怖い部分かもしれないですね。

ワークフロー

あーそうですよね、ワークフロー。できそうな気もします。あんまりちょっとしっくり来ていませんが。。。複数階層のマルチシグアカウントで実験してないからかもしれません。

どこまで署名が集まったか? っていうのは恒久的に確認できるんでしょうか。。。どこかで見れるのかも知れませんが、ちょっとまだ追えてないです。

その他

僕がもともとSymbolの機能で「ああ、こういうのあるのよねー」と思っていたのはネームスペース、モザイク、アグリゲートトランザクション、マルチシグのであったため、ここまでで一通りの? 「僕が思うSymbolの機能たち」というところまできました。長かった。

また次回。あと4章。ひとつきに2章やれればなんとか、12月には終えそうですね。

8. ロック

速習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も最初と同じ数量になる)

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

7. メタデータ

速習Symbolブロックチェーン7章、メタデータ編です。

URLとしてはこちら

今回は短そうですね。張り切ってやっていきます。

アカウント・モザイク・ネームスペースに対してKey-Value形式のデータを登録できる機能らしいです。

なるほど。わかるようなわからないような。なんかユースケースあるんだろうな。

実践編

7.1 アカウントに登録

aliceのアカウントに登録

アカウントに対してkey-value値を設定できるらしいです。

// aliceがaliceのメタデータを登録する。

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

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

// メタデータキーを作成する。
var metadataKey = sym.MetadataKey("key-chan");

// 値を指定する。
var value = "value-chan";
var valueBytes = Uint8List.fromList(utf8.encode(value));

var tx = sym.AccountMetadataInfoV1.create(alice.publicAccount, 
  alice.address, 
  metadataKey, 
  sym.Int16(valueBytes.length), 
  valueBytes);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100) );     // 手数料は100%。

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

await transactionSender.sendAggregateCompolete(transSetting, [tx]);

こんな感じで。

登録先、登録者が同一でもアグリゲートである必要があるらしいです。
いまいち、理由はわからないのですが。。

で、実行結果は以下。ウォレットでの確認で。。。

実行したときは何日も前で、ログを取り忘れていて、既に登録されたあとでした。

aliceがbobのアカウントに登録

自分自身のアカウントではなく、aliceがbobのアカウントにメタデータを登録するようなことも可能なようです。

モザイクを利用せずとも、メタデータを利用することで「会員の有効期限をメタデータに刻む」みたいなことができるのかな。

ただし、aliceがbobのアカウントにメタデータを刻む場合、bobの署名も必要(つまり、bobの承認が必要)であるため、好き勝手に追加するわけにはいかない、ということの様子。

ここではaliceがbobのアドレスに向けて、”kyuuri”というキーで”cucumber”という値を刻んでいこうと思います。

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

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

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

// メタデータキーを作成する。
var metadataKey = sym.MetadataKey("kyuuri");

// 値を指定する。
var value = "cucumber";
var valueBytes = Uint8List.fromList(utf8.encode(value));

var tx = sym.AccountMetadataInfoV1.create(alice.publicAccount, 
  bob.address, 
  metadataKey, 
  sym.Int16(valueBytes.length), 
  valueBytes);

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

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

await transactionSender.sendAggregateComplete(transSetting, [tx], [bob]);

トランザクション設定として連署者数を1とか設定しています。
(フィーリングで適当に、必要になったら追加しよう的なスタンスでやっているため、このときまで作ってませんでした)

実行結果は以下。とりあえず承認されたようです。

7.2 モザイクに登録

同様にモザイク相手にも、メタデータを設定できるようです。

以前の章でaliceが作ったモザイクに対し、キーを”daigouji”、値を”gai”で登録していこうと思います。

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

// 以前作ったモザイクID。
var mosaicId = sym.MosaicId("50E29F813AA1B901");

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

// メタデータキーを作成する。
var metadataKey = sym.MetadataKey("daigouji");

// 値を指定する。
var value = "gai";
var valueBytes = Uint8List.fromList(utf8.encode(value));

var tx = sym.MosaicMetadataInfoV1.create(alice.publicAccount, 
  alice.address, 
  mosaicId, 
  sym.Int16(valueBytes.length), 
  metadataKey,
  valueBytes);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100)
  );     // 手数料は100%。

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

await transactionSender.sendAggregateComplete(transSetting, [tx], []);

実行結果。結果確認は後ほど。

7.3 ネームスペースに登録

同様にネームスペースに登録します。

以前作成したネームスペース”murakamiharuki”に、キーを”murasaki”、値を”shikibu”として追加します。

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

// 以前作ったネームスペースID。
var namespaceId = sym.NamespaceId("murakamiharuki");

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

// メタデータキーを作成する。
var metadataKey = sym.MetadataKey("murasaki");

// 値を指定する。
var value = "shikibu";
var valueBytes = Uint8List.fromList(utf8.encode(value));

var tx = sym.NamespaceMetadataInfoV1.create(alice.publicAccount, 
  alice.address, 
  namespaceId, 
  sym.Int16(valueBytes.length), 
  metadataKey,
  valueBytes);

// Transaction設定を行う。  
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee:sym.FeeMultiplier(100)
  );     // 手数料は100%。

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

await transactionSender.sendAggregateComplete(transSetting, [tx], []);

実行結果は以下。

とりあえずすべて、承認されたようでした。

7.4 確認

登録したメタデータを確認するようです。

restapiに条件を投げ入れて、内容を確認します。

aliceにづいたメタデータをすべて取得し、メタデータのキー、バリュー、取得した1レコードの全体を出力しています。

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

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

var networkHostInfo = await TransactionSender.getNetworkHost();
var metadataHttp = sym.MetadataRoutesHttp(networkHostInfo.networkHost);

var criteria = sym.SearchMetadataCriteria();
criteria.targetAddress = alice.address;
criteria.sourceAddress = alice.address;

var result = await metadataHttp.search(criteria);

for (var element in result.data){
  var metadata = element.metadataEntry;
  print("メタデータキー: ${metadata.scopedMetadataKey.value}");
  print("メタデータバリュー: ${utf8.decode(hex.decode(metadata.value.value))}");
  print("データ全体");
  print(metadata.baseMap);
}

上からメタデータバリューが

  • value-chan
  • gai
  • shikibu

となっていることが確認できます。

速習Symbolによると、metadataTypeの値により、なんのメタデータかを判別することができるようです。

sym.MetadataType
{0: 'Account', 1: 'Mosaic', 2: 'Namespace'}

“value-chan”はmetadataTypeが0となっているため、アカウントに紐づいていることが確認できます。

同様に”gai”はモザイク、”shikibu”はNamespaceということが確認できます。以上です。おつかれさまでした。

感想編

メタデータはキーバリュー型で値を登録することができるため、汎用性が高そうですね。いくつまで登録できるのだろう?

現場で使えるヒントで有資格証明の話がありました。

“実社会で信頼性の高いドメインからリンクされたアカウントが発行したメタデータの付与を受けることで、そのドメイン内での有資格情報の所有を証明できる”

例えばアドレスNAAAAからNBBBBにキー”友人証明”というメタデータを与えて、NBBBBが承認した場合、
NAAAAがNBBBBに対して友人証明というメタデータを与えただけという事象になる。

それを仮に、NAAAAにはkishidaというネームスペースが付与してあり、実際にkishidaというネームスペースでいろいろな活動(評価)を受けていたとして。

同様に、NBBBBにはtakashiというネームスペースが付与してあり、実際にいろいろな活動(評価)を受けていたとした場合。

kishidaがtakashiに友人証明というメタデータを登録することで、kishidaはtakashiと友人である、ということになる。

わかりづらいな。。。

これが仮にkishidaを英検の認定試験者、友人証明を英検合格証明、値として級とした場合、

英検の認定試験者(ネームスペース・アドレス)が、英検の合格者(ネームスペースorアドレス)に、英検合格証明(メタデータキー)として1級(メタデータの値として1)を登録した。

とすることで、価値がでるよね、って話でしょうかね。

DIDはちょっと、すぐにはわかりませんでした。なんか他でもそんな話があったので、後日理解に勤めます。。。

メタデータはアグリゲートトランザクションで実行する必要がある、というのは原則として、他アドレスに付与するのがメインの用途、という感じなんでしょうか?

処理上の問題であったらわからないですが。

また、メタデータキーとしてはハッシュで取得するようなので、元々の文字列は不可逆ということでしょうか。

別に戻せる必要はない、と言われたらそうかもしれないですが。。。

あまり考えずに行き当たりばったりでやっているため、まだ作っていないところがボロボロ出てきて時間がかかりました。

本当は速習Symbolを夏までに終わらせたいと思っていたのですが、なかなかうまくはいかないものですね。年内に終わればいいですけれど。

お疲れさまでした。

6. ネームスペース

速習Symbolブロックチェーン6章、ネームスペース編です。

URLとしてはこちら

テストネットの紹介はやめます。
(全然終わりそうもないので、進めるのを優先します)

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

実践編

ネームスペースをレンタルし、アドレスやモザイクに名前を付けることができるということみたいです。

Symbolブロックチェーンではネームスペースをレンタルしてアドレスやモザイクに視認性の高い単語をリンクさせることができます。 ネームスペースは最大64文字、利用可能な文字は a, b, c, …, z, 0, 1, 2, …, 9, _ , – です。

Symbolerなら見たことあるかもしれません。

小文字しかできないのかな?

6.1 手数料の計算

ネームスペースのレンタルには手数料が必要なようです。ブロックチェーンに刻む=手数料が必要、そんな感じですね。参照は無料です。

こんな感じのコードを記述しました。

  /// レンタルフィーを取得する。
  /// [rentalDays]にレンタルする日数を指定する。(30日以上とする必要がある)
  /// ※ここでは、rentalDaysは365を指定した。
  Future<void> getRentalFees(int rentalDays) async {

    // 有効なネットワークホストを取得する。
    var availableHost = await _getNetworkHost();

    // レンタルフィーはNetworkRoutesから取得する様子。
    var networkRoute = NetworkRoutesHttp(availableHost.networkHost);

    // レンタルフィーはRootNamespace、SubNamespace、Mosaicで違うようだ。
    var rentalFees = (await networkRoute.getRentalFeesInformation());

    // レンタル日数を秒に戻し、30(Symbolのブロック生成速度は30sec)で割り、ブロック数を割り出す。
    var rentalBlock = rentalDays * 24 * 60 * 60 / 30;

    var rootRentalFee = rentalFees.effectiveRootNamespaceRentalFeePerBlock.value * rentalBlock;
    var childRentalFee = rentalFees.effectiveChildNamespaceRentalFee;

    print("rentalBlock: ${rentalBlock}");
    print("rootRentalFee: ${rootRentalFee}");
    print("childRentalFee: ${childRentalFee.value}");

  }

実行結果は以下。

ネームスペースレンタルフィー手数料は210240000と算出されます。

Symbolノードを利用しているため、手数料に使用するXYMは可分性が6であることから、1/1000000します。つまり、210.24XYMが手数料になります。速習Symbol通り。

また、ネームスペースは日付を指定した親(Root)ネームスペースと、紐づいた子(Child)ネームスペースが用意できるようです。

期間が指定できるのは親のようなので、親が生きている限り子も生きているということでいいのかな、と。

で、親がいる限りは子の追加は子(childRentalFee)のみで実行できるということでしょうか。10XYMかな?

6.2 レンタル

ルートネームスペース/サブネームスペースをレンタルするようです。

ルートネームスペースの子どもとして、サブネームスペース、サブのサブネームスペースぐらいの階層までは取得できるようです。

なお、ルート/サブのネームスペース取得には手数料がかかることがわかります。

また、ネームスペースの有効期限はルートネームスペースに依存しているとのことで、サブよりルートのネームスペースのレンタル料は高くなっています。

有効期限の更新についても、ルートネームスペースの期限さえ気にしていればよさそうですね。

さて、

  • murakamiharuki(ルート)
  • sekai-no-owari(サブ)
  • wonder-land(サブサブ)

という形で取得してみたいと思います。
murakamiharuki.sekai-no-owari.wonder-landという完成形ですね。

また、レンタルする期間は1051200ブロックを考えています。

以下、ソースコード。

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

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

// 大体、一年間分の期間。
var rentalDuration = sym.BlockDuration(1051200);

// ルートネームスペース。
var rootNamespaceId = sym.NamespaceId("murakamiharuki");

// サブネームスペース。
var subNamespaceId = sym.NamespaceId("murakamiharuki.sekai-no-owari");

// サブサブネームスペース。
var subsubNamespaceId = sym.NamespaceId('murakamiharuki.sekai-no-owari.wonder-land');

// トランザクション。
var transactions = [
  // ルートネームスペース登録トランザクション。
  sym.NamespaceRegistrationInfoV1.createRoot(alice.publicAccount, rentalDuration, rootNamespaceId, rootNamespaceId.fullName!),
  // サブネームスペース登録トランザクション。
  sym.NamespaceRegistrationInfoV1.createSub(alice.publicAccount, rootNamespaceId, subNamespaceId, subNamespaceId.name!),
  // サブサブネームスペース登録トランザクション。
  sym.NamespaceRegistrationInfoV1.createSub(alice.publicAccount, subNamespaceId, subsubNamespaceId, subsubNamespaceId.name!)
];

// Transaction設定を行う。
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(1667250467),
  fee: sym.FeeMultiplier(100) );     // 手数料は100%。          


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

for (var element in transactions) {
  await transactionSender.sendTransaction(transSetting, element);
}

作りの問題で、alice.publicAccountが複数でてきていますね。
これは不要なので、そのうち直したいですが。。。

では実行してみます。

ルートネームスペース取得までの結果は以下。

サブネームスペースは以下。

サブサブネームスペースは以下。

一応ウォレットで確認すると、作成されていることが確認できます。

。。。たぶんこれでいいんだよね? というところ。

有効期限の計算

先ほどの通り、有効期限はルートネームスペースに依存している、ということのようです。

ブロックチェーンのタクトとしては「時間」ではなく「ブロック高」ということなので、

  1. ネームスペース情報を取得し、有効期限のブロック数を取得する。
  2. 現在の最後のブロック数を取得する。
  3. ブロックに関する情報を取得することで、ブロックが生成された時刻を取得する。
  4. (ブロックが生成された時刻 + 現在から有効期限終了までのブロック数スパンを時間化)とすることで、予定時刻が出せるようです。

なお、ネームスペースをレンタルするトランザクション発行から、この有効期限の計算を行うまでいくらか時間経過しています。うーむ。

var networkHostInfo = await TransactionSender.getNetworkHost();

var namespaceHttp = sym.NamespaceRoutesHttp(networkHostInfo.networkHost);
var chainHttp = sym.ChainRoutesHttp(networkHostInfo.networkHost);
var blockHttp = sym.BlockRoutesHttp(networkHostInfo.networkHost);

var namespaceId = sym.NamespaceId("murakamiharuki");

// ネームスペース情報を取得する。
var nsInfo = await namespaceHttp.getInformation(namespaceId);

var lastHeight = (await chainHttp.getCurrentInformation()).height;

var lastBlock = await blockHttp.getInformation(lastHeight);

var remainHeight = nsInfo.namespace.endHeight - lastHeight;

print("namespace endHeight: ${lastHeight.value}");
print("lastBlock: ${lastBlock.block.height.value}");

print("remainHeight: ${remainHeight.value}");

var timestamp = lastBlock.block.timestamp.value + (remainHeight.value * 30000 + epochAdjustment * 1000);

var endDate = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt(), isUtc: false);

print("endDate: $endDate");

取得した結果がこちら。

まぁこんなもんでしょうか。

念のため本日(20230701)のウォレットからの確認結果です。

まぁ、大体あってると思います。

6.3 リンク

ネームスペースは取得しただけでは活用法がない? ため、特に意味はないかも?

ネームスペースはアドレスやモザイクと紐づくことで、それに命名できる感じなようです。便利ですね。

アカウントへのリンク

アカウントへというか、アカウントのアドレスみたいですね。

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

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

// ネームスペース。
var namespaceId = sym.NamespaceId("murakamiharuki");

// アドレスエイリアストランザクション。
var tx = sym.AddressAliasInfoV1.create(alice.publicAccount, namespaceId, alice.address, sym.AliasActionEnum.link);

// Transaction設定を行う。
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee: sym.FeeMultiplier(100) );     // 手数料は100%。          

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

// トランザクション送信。
await transactionSender.sendTransaction(transSetting, tx);

ウォレットで確認したところ、下記のようでした。

エイリアスタイプがaddressでエイリアスにアドレスが登録されていることが確認できました。

モザイクへリンク

そのまま続けてモザイクへリンクを行います。

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

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

// モザイクid。以前の章で作成済み。
var mosaicId = sym.MosaicId("50E29F813AA1B901");

// ネームスペース。世界の終わり。
var namespaceId = sym.NamespaceId("murakamiharuki.sekai-no-owari");

// アドレスエイリアストランザクション。
var tx = sym.MosaicAliasInfoV1.create(alice.publicAccount, namespaceId, mosaicId, sym.AliasActionEnum.link);

// Transaction設定を行う。
var transSetting = sym.TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee: sym.FeeMultiplier(100) );     // 手数料は100%。          

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

// トランザクション送信。
await transactionSender.sendTransaction(transSetting, tx);

実行結果は以下。

とりあえずトランザクションが承認されている様子。

ウォレットで確認したところ、以下となっていました。

エイリアスタイプがmosaicで、エイリアスにモザイクIDが設定されています。

ウォレットのモザイクページで確認したところ、モザイクにも名前が紐づいていることが確認できます。

6.4 未解決で使用

アカウント(アドレス)、モザイクIDにネームスペースをリンクすることで、わかりやすく名前を紐づけることができるようになりました。

この未解決で使用の項では、ネームスペース→アドレスやネームスペース→モザイクIDと変換しなくても、ネームスペース=アドレス、ネームスペース=モザイクIDとして直接送信できるようですね。

ここいらでunresolved addressって名前が出てきたりするのですが、unresolved addressは未解決って意味らしいです。へー。

以下はコード。

print("bobがaliceに送信する");

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

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

// 未解決アドレス(alice)
var unresolvedAddress = sym.NamespaceId("murakamiharuki").getUnresolvedAddress(sym.NetworkTypeEnum.testnet);

// 未解決モザイク(murakamiharuki.sekai-no-owari)
var unresolvedMosaicId = sym.NamespaceId("murakamiharuki.sekai-no-owari").getUnresolvedMosaicId();

// 転送モザイク。
var transMosaic = sym.Mosaic(amount: sym.Amount(100), id: unresolvedMosaicId);

// Transaction設定を行う。
var transSetting = sym.TransactionSetting(
  signer: bob, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: sym.NetworkTypeEnum.testnet, 
  deadline: sym.Deadline.create(epochAdjustment),
  fee: sym.FeeMultiplier(100) );     // 手数料は100%。    

// 送り先に未解決アドレス、未解決モザイクを設定する。
var tx = sym.TransferInfoV1.create(bob.publicAccount, unresolvedAddress, [transMosaic] );

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

// トランザクション送信。
await transactionSender.sendTransaction(transSetting, tx);

速習Symbolでは未解決アドレス、未解決モザイクでやっていましたが、横着して一度にやることにしました。

送信するモザイクはsekai-no-owariに紐づいたモザイクIDです。

今回はbob → alice(未解決アドレス)と送信するため、あらかじめaliceからbobへ、sekai-no-owariをウォレットにて送信しておきました。

いざ実行です。

とりあえず承認されました。

ウォレットで確認します。

murakamiharukiあてに送られているのは確認できました。

sekai-no-owariモザイクはなんか表示がバグってますね(ウォレットが古い?)

数はしっかり減っていたため、explorerで見ると転送されていることが確認できました。

6.5 参照

ネームスペースのリンク情報を参照する方法についてです。

以下の感じです。

var namespaceId = sym.NamespaceId("murakamiharuki");
var namespaceId2 = sym.NamespaceId("murakamiharuki.sekai-no-owari");

var networkHostInfo = await TransactionSender.getNetworkHost();
var namespaceHttp = sym.NamespaceRoutesHttp(networkHostInfo.networkHost);

// ネームスペース情報を取得する。
var nsInfo = await namespaceHttp.getInformation(namespaceId);
var nsInfo2 = await namespaceHttp.getInformation(namespaceId2);

print("murakamiharuki info");
print(nsInfo.baseMap);

print("murakamiharuki.sekai-no-owari info");
print(nsInfo2.baseMap);

結果は以下。

aliasのtypeが2でアドレスエイリアス、addressに紐づいている対象アドレスでしょうか。

aliasのtypeが1でmosaicIdって形のようですね。

ネームスペース → 紐づき状態を確認できるって感じですね。

また、速習SymbolによるとregistrationTypeで親か子かもわかるようです。

逆引き

ネームスペース → アドレスと取得できるのであれば、逆にアドレス → ネームスペースと取得することができる、とのこと。

ネームスペースをアドレスに紐づける場合、「紐づき先のアドレスはネームスペース所有者以外でもよい」とのことなので、ネームスペース:アドレスは1:Nの関係であるらしいですね。

ここまで使用しているaliceのアドレスと、今回ネームスペースとリンクしたモザイクに付与されているネームスペースを取得します。

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

// ネームスペースが紐づいているモザイクID。
var mosaicId = sym.MosaicId("50E29F813AA1B901");

print("[取得元] alice address: ${alice.address}");

var networkHostInfo = await TransactionSender.getNetworkHost();
var namespaceHttp = sym.NamespaceRoutesHttp(networkHostInfo.networkHost);

// アカウント名を取得。
var accountNames = await namespaceHttp.getReadableNamesFromAddresses([alice.address]);

// モザイク名を取得。
var mosaicNames = await namespaceHttp.getReadableNamesFromMosaics([mosaicId]);

print("account names");
print(accountNames.baseMap);

print("mosaic names");
print(mosaicNames.baseMap);

結果としては以下。

accountNamesも、mosaicNamesも、内容が配列になっているため、複数の情報を一度に取ることができることが確認できます。

レシートの参照

トランザクションに使用されたネームスペースを、ブロックチェーンがどうやって解決したかを確認するらしいです。

ネームスペースAは今、浜田くんと紐づいている。

しかし期限が切れた後、ネームスペースAを松本くんがレンタルして自分に紐づけた場合、同じネームスペースAであっても紐づけ先が変わっている。

そのため、現在の状態で過去の情報を引っ張ってきても、送り先が変わっている可能性がある、ということかな。

電話番号みたいですね。

以下のコードは、ブロック高が597274に対して、アドレス解決とモザイクIDの解決内容を取得するようにしました。

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

// ネームスペースが紐づいているモザイクID。
var mosaicId = sym.MosaicId("50E29F813AA1B901");

print("[取得元] alice address: ${alice.address}");

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

var criteria = sym.GetReceiptsAddressResolutionStatementsCriteria();

// ブロック高を597274を指定(bobがaliceにsekai-no-owariを送付したブロック)。
criteria.height = sym.Height(597274);

var receipt1 = await receiptHttp.getAddressResolutionStatements(criteria);
var receipt2 = await receiptHttp.getMosaicResolutionStatements(criteria);

print("receipt address");
print(receipt1.baseMap);
// わかりづらいのでエンコード済みアドレス化。
print(receipt1.data[0].statement.resolutionEntries[0].resolved.toAddress().value);

print("receipt mosaicId");
print(receipt2.baseMap);

結果は以下。アドレスも、モザイクIDも取得されていることがわかるため、これで当時の紐づき状態を知ることができるようです。

6.6 現場で使えるヒント

読みました。ドメインのような「個人を識別しやすくする」ということでしょうか。あとは譲渡について、というところ。

感想編

ざっくりといえば、

  • Symbolにはネームスペースという概念がある。
  • ネームスペースは3階層まで指定可能。(ネットワーク設定による?)
  • ネームスペースはレンタル制で、レンタル期間はルートネームスペースの期限で定めている。
  • ネームスペースにはアドレス、モザイクIDを紐づけることができる。
  • 紐づけることで、アドレスやモザイクIDに任意の名前を付けることができる。
  • ネームスペースについてはチェーン上で一意である。

という機能のようですね。

基本的にアドレス、モザイクID自体には「名前」といったものがないので、それを任意に付与する、というもののようです。

ネームスペースIDは文字列から生成され、かつ、一意であるため、使用する名前=IDのようです。

現場で使えるヒントからすると、ネームスペースのレンタルを途中でやめる、という選択肢はなさそう?
同様にネームスペースの譲渡ということもできない模様。

よって、譲渡する可能性のあるネームスペースであった場合、
マルチシグの子アカウントを作成し、子アカウントごとマルチシグで譲渡する。そういう風にするようです。

未解決状態でアドレス、モザイクIDとして扱えるのは面白いですが、レンタル期間の終了、または意図しないものとの紐づきに注意する必要もありそうですね。

ただ実際に使用する際にはレンタル期間が切れている可能性もありますし、タイポの可能性もある? とかって考えると、ひと手間かかりますが、リンク状態を確認してから使用するほうが安心な気もしますね。

以上です。また次回。

5. モザイク

速習Symbolブロックチェーン4章、モザイク編です。

URLとしてはこちら

今回はテストネットの

http://sym-test-01.opening-line.jp/

をお借りしました。

オープニングライン社さんですね。

ブロックチェーンを使用した、ファイルを受け渡すJUGGLEが話題(2023/05/27時点)です。

テストネットを用意している方は素敵な方ではあるので、一言紹介していきたいんですよね。

ただ、このまま続けていくと(僕が誰か判別できる)テストネットノードはすぐに枯渇しちゃいそうです。

うちのノード使ってもいいよ、という方がいたら教えてください。(ここで一言紹介してもしょうがないかもしれませんけど。

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

実践編

5.1 モザイク作成

モザイクの作成と、作成された状態を確認していきます。

まずコード。

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

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

// モザイクの設定を指定する。
// 指定したフラグは有効状態とする。
var mosaicFlag = 
  MosaicFlagsEnum.supplyMutable +     // 供給量変更可能
  MosaicFlagsEnum.restrictable +      // 制限設定可能
  MosaicFlagsEnum.revocable;          // 発行者からの回収可能
// ここでは指定していないが、transferable(第三者への譲渡)も設定できるらしい。

var nonce = MosaicNonce.createRandom();

var mosaicId = MosaicId.createNew(nonce, alice.address);

// tx1
// モザイクの設定を行う。
var mosaicDefInfo = MosaicDefinitionInfoV1.create(alice.publicAccount, mosaicId, 
    BlockDuration(0), nonce, mosaicFlag, Int8(2));

// tx2
// モザイクの数量を増加する。(おそらく、設定だけでは0? そもそも設定だけでは受け取らない?)
var mosaicSupplyInfo = MosaicSupplyChangeInfoV1.create(alice.publicAccount, mosaicId, 
    Amount(100000000), MosaicSupplyChangeActionEnum.increase);

// トランザクションをまとめる。
var aggregate = [mosaicDefInfo, mosaicSupplyInfo];

// Transaction設定を行う。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(1667250467),
  fee:FeeMultiplier(100) );     // 手数料は100%。          

var mosaicWorker = MosaicWorker();

await mosaicWorker.sendTransaction(transSetting, aggregate);

基本的な設定はほぼ、速習Symbolと同じだと思っています。

作成するモザイクは

  • 供給量変更可能
  • 制限設定可能
  • 発行者からの回収可能

※これらを具体的に実行できる条件はわからないのですが。

nonceはランダム、

tx1は速習通りモザイクの設定、

tx2はモザイク数量の増加としています。

divisibilityに2を設定したため小数点以下は2桁まで、

数量は100000000としたため、発行量は1,000,000.00としました。百万。

細かいソースを追っても仕方ないのですが、トランザクション発行部分は下記。


var availableHost = await _getNetworkHost();

var aggCompleteInfo = AggregateCompleteInfoV2(availableHost.networkType, transInfos);

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

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

var transRouteHttp = TransactionRoutesHttp(availableHost.networkHost);

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

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(aggregateTransaction.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;
    default:
      print("パーシャル!!! パーシャル……?");
      return;
  }
}

print("承認後はTransactionRouteから承認トランザクションの内容を取得することが可能になっている。");

var confirmedInfo = await transRouteHttp.getConfirmedInformation(aggregateTransaction.transactionHash.value);

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

(前の章で実施した)アグリゲートコンプリートトランザクションで発行し、その承認結果を確認します。

それでは実行します。

こんな感じです。少なくともトランザクションの承認は行われたようです。

これを、念のためにウォレットでも確認してみます。

aliceのウォレットのアセット欄に、symbol.xym以外のトークンが表示されていますね。

見づらいかもしれませんが、先ほどの一部を抜粋すると、

mosaicIdとしてある50E29F813AA1B901ということみたいです。

これでとりあえず、モザイクの生成は行われているようです。

また、数も1,000,000となっているため100万とでています。

たぶんこれで発行数の変更も実行されているかな、と思います。

5.3 現場で使えるヒント

所有証明について書かれています。ブロックチェーンがあると所有証明も可能であるとともに、NFTの発行方法などが記載されています。

NFT

というわけで、速習Symbolに倣ってNFTを発行したいと思います。

といっても、複製や数量変更できない、唯一のトークンを発行するということでしかないようですが。

以下のようにコードを設定しました。

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

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

// モザイクの設定を指定する。
// 指定したフラグは有効状態となる。
var mosaicFlag = 
  MosaicFlagsEnum.restrictable +      // 制限設定可能
  MosaicFlagsEnum.revocable;          // 発行者からの回収可能
// MosaicFlagsEnum.supplyMutable      // 供給量変更可能は指定しない。
// MosaicFlagsEnum.transferable       // 第三者への譲渡不可。

var nonce = MosaicNonce.createRandom();
// var nonce = MosaicNonce.create(Uint8List.fromList([210,53,66,166]));

var mosaicId = MosaicId.createNew(nonce, alice.address);

// tx1
// モザイクの設定を行う。
// 可分性0、有効期限無制限。
var mosaicDefInfo = MosaicDefinitionInfoV1.create(alice.publicAccount, mosaicId, 
    BlockDuration(0), nonce, mosaicFlag, Int8(0));

// tx2
// モザイクの数量を増加する。
// 数量は1。可分性が0のため、1は1。
var mosaicChangeInfo = MosaicSupplyChangeInfoV1.create(alice.publicAccount, mosaicId, 
    Amount(1), MosaicSupplyChangeActionEnum.increase);

// tx3
// NFTデータ
var transInfo = TransferInfoV1.create(alice.publicAccount, alice.publicAccount.address, [],
  PlainMessage.create("Hello Symbol!!!"));

// トランザクションをまとめる。
var aggregate = [mosaicDefInfo, mosaicChangeInfo, transInfo];

// Transaction設定を行う。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(1667250467),
  fee:FeeMultiplier(100) );     // 手数料は100%。          

var mosaicWorker = MosaicWorker();

// トランザクションの通知。
await mosaicWorker.sendTransaction(transSetting, aggregate);

mosaicWorkerについては省略します。

同じように? 設定したつもりのため、これで実行してみます。

実行されました。ウォレットで確認すると先ほどの通り、数量は1となっています。

そして生成と同じブロックに投入したメッセージ=NFTのデータとすることで、データ付きのNFTにすることができる、ということのようです。うーむ、なるほど。。。

回収可能なポイント

先ほどの唯一発行したトークンをbobへと送信しておきます。

以下、bobのウォレット状態です。

365~~が先ほどのNFTですね。たぶん。

さて、こいつを回収できるらしいので、aliceが回収を行います。

さくっとこんな感じ。

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

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

// 回収トランザクション。対象アドレスはbob。数量は1。
var revocationTx = MosaicSupplyRevocationInfoV1.create(alice.publicAccount, 
  Address("TBZ7AEGPLAP2YCVPSS6CJR27EVOQ7M4AXULACRY"), 
  Mosaic(id: MosaicId("365F2BE795816BAE"), amount: Amount(1)));

// Transaction設定を行う。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(1667250467),
  fee:FeeMultiplier(100) );     // 手数料は100%。          

var mosaicWorker = MosaicWorker();

// トランザクションの通知。
await mosaicWorker.sendTransaction(transSetting, revocationTx);

トランザクションの通知周りはもう省略していきます。

特に変更がなければ、アグリゲートもベーシックも通知 -> 承認を探す、取得の流れのため。

実行します。

実行完了は特に実感がなかったのですが、bobのウォレットを見るときっちりモザイクが消えていました。

Explorerで見るとちゃんと、回収したトランザクションは掲載されていました。

なお、このトークンは消滅させるわけではなくあくまでも回収のため、

aliceの元へと戻っていました。

モザイクの実践編は以上です。

感想編

おお、モザイク作成って手ごろなんだなぁ。ってところです。名前を付けるのはまだ先なので、MosaicIdのみでの表示ですが。

モザイク=あらゆるものの動きや状態を示すことができるのだろうなぁ、と実感を持ちました。

別に本筋とは関係ないのですが、MosaicIdは先頭ビットが0、NamespaceIdは先頭ビットが0にするというメモが僕の手元にありました。どこで見たのか覚えてないのですが。

ビット演算なんてそうそう使う機会ない? もんな。。。

モザイクの使い方としては、会員証・権利のようなものや、ポイントのようなもの。

アドレス=場所と紐づけたらトレーサビリティにも使用できるでしょうし、カンバン的にも使用できるかもしれないですね。あとは在庫管理とか?

これをパブリックでやるかどうかという話はあるかと思いますが、とても奥が深そうだなと思います。

以上です。

4. トランザクション

速習Symbolブロックチェーン4章、トランザクション編です。

URLとしてはこちら

……コミュニティサイトのほうにしたほうがいいのかな?

今回から、実施編と感想編を分けないことにしました。

書くのが面倒だからね。

今回はテストネットの

http://mikun-testnet.tk/

をお借りしました。

Symbol/NEMでは知らない人がいない?

@mikunNEM さんのテストネットノードです。ありがとうございます。

もちろんメインネットのノード運営も行っている方です。

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

前回のアカウント編ではRawAddressの意味について認識の違いがありました。
※いずれ直すかも。

実施編

トランザクションのライフサイクル

トランザクションを作成してから、ファイナライズまでの流れが掲載されています。

説明を図に起こした感じです。

クライアント側の処理としてはトランザクション作成から署名、アナウンスするところまで。

そこから先はノード側の仕事、という話です。

なお、ファイナライズを迎えるまではロールバックする可能性がありますよ、とのことでした。確率上、どの程度おきるかはわからないですが。

また、1手続きは1トランザクションとして扱われますが、最終的なデータとしてはブロック単位で同期されるとのことでした。ブロックには複数のトランザクションを取り込まれる(可能性がある)。。ということでいいんでしょうかね。

4.2 トランザクション作成から、4.4 確認まで。

基本的なトランザクションとして、aliceからbobへと転送トランザクションを作成し、アナウンスするということです。

トランザクションの作成には有効期限・最大手数料が必要であること。転送トランザクションにはメッセージを付与することが可能であることが掲載されています。

具体的には先ほどの図のトランザクション作成から、承認済みトランザクションとなるまでの実行方法が内容が掲載されています。

すみません、それに合わせて記載するのがちょっと難しかったため、

相変わらず伝わらないソースでも掲載しておきます。

// aliceのアカウントを作成する。(送信元)
var alice = await Account.createFromPrivateKey(
  PrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178E*****"), NetworkTypeEnum.testnet);

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

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

// 送信するモザイク情報を決定する。
List<Mosaic> mosaics = [];
mosaics.add(Mosaic(id: MosaicId("72C0212E67A08BCE"), amount: Amount("100")));

// 送信するメッセージを構築する。
var message = PlainMessage.create("Hello Symbol-chan.");
var crypt = await message.crypt(alice.privateKey, bob.publicAccount);

// 送信内容を構築した。
var transInfo = TransferInfoV1.create(alice.publicAccount, bob.address, mosaics, crypt);

var deadline = Deadline.create(1667250467);     // testnetのepochAdjustment

var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: deadline,
  fee:FeeMultiplier(100) );     // 手数料は100%。

var transWorker = TransactionWorker();

await transWorker.transferTransaction(transSetting, transInfo);

見づらいかもしれませんが、
ざっくりいえばテストネットでaliceからbobに100だけモザイク(テストネットXYM)を送信します。実際は100といっても0.000000まで有効なため、0.0001になるのですが。

メッセージとしては”Hello Symbol-chan.”を暗号化して送信すること、手数料は100%であることを示しています。

最後のtransWorker.transferTransactionは以下のような感じです。

  /// 転送トランザクションを実行する。
  Future<void> transferTransaction(TransactionSetting transSetting, TransferInfoV1 transInfo) async {

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

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

    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;
        default:
          print("パーシャル!!! パーシャル……?");
          return;
      }
    }

    print("承認後はTransactionRouteから承認トランザクションの内容を取得することが可能になっている。");

    var confirmedInfo = await transRouteHttp.getConfirmedInformation(basicTransaction.transactionHash.value);

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

  }

別にtransferTransactionなんて名前にする必要はなかったんですけど。

SymbolSDKの作り方を半分無視しているので、こんなよくわからない感じになっています。

上のコードでトランザクション内容を作っています。

下のコードでBasicTransaction(なにかというとAggregateTransactionでない単一のTransaction)とし、それをノードにアナウンスし、承認待ちをしています。

なお、エラーの場合は考えてません。(エラーの場合に苦労した感)

そして実行した結果は以下のようになりました。

問題なく、トランザクションがアナウンスされ、承認されています。

なお、bobのウォレットから、トランザクション内容の確認は以下(メッセージの復号済み)

メッセージとモザイク数量が指定した通りに出力されていることが確認できます。

4.5 トランザクション履歴

トランザクション履歴を一覧で取得する。

アカウント情報の作成を行い、トランザクション履歴取得メソッド(searchConfirmedTransactions)を呼び出す。

// aliceのアカウントを作成する。(送信元)
var alice = await Account.createFromPrivateKey(
  PrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178******"), NetworkTypeEnum.testnet);

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

var transWorker = TransactionWorker();

await transWorker.searchConfirmedTransactions(alice.address);

searchConfirmedTransactionsの内容は以下の感じで。

var availableHost = await _getNetworkHost();

var transRouteHttp = TransactionRoutesHttp(availableHost.networkHost);

// 取得する条件を設定する。
var criteria = SearchConfirmedTransactionsCriteria();

criteria.address = targetAddress;
criteria.embedded = true;
criteria.order = OrderEnum.desc;

// 承認済みトランザクションを取得する。
var result = await transRouteHttp.searchConfirmed(criteria);

print("トランザクション履歴を表示する");
for (var transInfo in result.data){
  print(jsonEncode(transInfo.baseMap));     
}

取得先はどのAPIにすればいいのかわからなかったので、とりあえずTransatcion RoutesのSearch confirmed transactionとしました。

条件としてアドレス、embeddedをtrue、orderをdescにしています。

※成功するまでにいろいろと何度も実行したため、最新トランザクションがわからなくなるのでdescとしました。

取得したresultにはページ番号なども取得できているのですが、内容のdata部分だけで出力しています。

取得結果はこんな感じになりました。複数のトランザクション履歴が取得できます。

実は、前の項目で送信した転送トランザクション(448486ブロック)後に、別のトランザクションを発行してしまいまして、、、。

前の項目で送信したのは2件目になっていました。

よって2件目を抜粋します。

{
	"meta": {
		"height": "448486",
		"hash": "96C0FBA669287EFB7395EFE9AC22D061DC8A5E2154255DD72815017868C5A750",
		"merkleComponentHash": "96C0FBA669287EFB7395EFE9AC22D061DC8A5E2154255DD72815017868C5A750",
		"index": 0,
		"timestamp": "16387287287",
		"feeMultiplier": 100
	},
	"transaction": {
		"size": 269,
		"signature": "47D6B206B5E0AFE4AA97D8168C90B30A45E8B6904264056D84866F9B596FACFC084C27DB517B95F74A8CF28C714028ED2C94CD5276EAC9832960DB050466BC01",
		"signerPublicKey": "67626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28",
		"version": 1,
		"network": 152,
		"type": 16724,
		"maxFee": "26900",
		"deadline": "16394482000",
		"recipientAddress": "9873F010CF581FAC0AAF94BC24C75F255D0FB380BD160147",
		"message": "013334344138353233323339463146353136334638344246463745344533304233333139333333343835393538364632333937303646393444303637433130424146393633454330333042373341454239354336313835303437314543",
		"mosaics": [
			{
				"id": "72C0212E67A08BCE",
				"amount": "100"
			}
		]
	},
	"id": "645A45FACF72001C11039B4F"
}

メッセージは暗号化されていますが、モザイクを100だけ送信しています。

transactionの内容を見ると、

networkが152(testnet)

typeが16724(transfer)

recipientAddress(転送先アドレス)、signerPublickey(署名者公開鍵)、

同様にmosaicsの中にモザイクidとamount(数量)の指定がしてあります。

どのテストネットであること、手続きが転送であること、転送先とモザイクIDの指定というのが見て取ることができます。

なお、一番上のトランザクションは、次の項目で実行したものです。

transactionのtypeが16705(aggregate complete)を示しているため、アグリゲートコンプリートトランザクションであることを示しています。

じゃあ次に行きましょう。

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

複数のトランザクションをまとめることができるのがアグリゲートトランザクションのようです。

速習Symbolではaliceがbob、carolに転送トランザクションを送信していましたが、まぁbobに2度送るのも同じだよね? ということで、aliceがbobに2度送る形にしました。

謎のコードとしては以下。

// aliceのアカウントを作成する。(送信元)
var alice = await Account.createFromPrivateKey(
  PrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178E*****"), NetworkTypeEnum.testnet);

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

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

// 作りが悪く、名前が被ってしまっているらしい。
List<trans.TransactionInfo> transInfos = [];

// TransferTrans 1.
{

  // 送信するモザイク情報を決定する。
  List<Mosaic> mosaics = [];
  mosaics.add(Mosaic(id: MosaicId("72C0212E67A08BCE"), amount: Amount("1")));

  // 送信するメッセージを構築する。
  var message = PlainMessage.create("tx1");

  // 送信内容を構築した。
  transInfos.add(TransferInfoV1.create(alice.publicAccount, bob.address, mosaics, message));

}

// TransferTrans 2.
{

  // 送信するモザイク情報を決定する。
  List<Mosaic> mosaics = [];
  mosaics.add(Mosaic(id: MosaicId("72C0212E67A08BCE"), amount: Amount("2")));

  // 送信するメッセージを構築する。
  var message = PlainMessage.create("tx2");

  // 送信内容を構築した。
  transInfos.add(TransferInfoV1.create(alice.publicAccount, bob.address, mosaics, message));

}

// Transaction設定。

var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(1667250467),
  fee:FeeMultiplier(100) );     // 手数料は100%。          

var transWorker = TransactionWorker();

await transWorker.aggregateTransferTransaction(transSetting, transInfos);

※V1ってつけてインスタンス化しているのがダサいので、いつか変えます。たぶん。

aliceがbobに2回転送トランザクションを送信します。

それぞれ、「メッセージに”tx1″、数量として1」「メッセージに”tx2″、数量として2」となっています。

トランザクションを送信している、aggregateTransferTransactionについては以下。


var aggregateComplete = AggregateCompleteInfoV2(NetworkTypeEnum.testnet, transInfos);

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

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

var availableHost = await _getNetworkHost();

var transRouteHttp = TransactionRoutesHttp(availableHost.networkHost);

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

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(aggregateTransaction.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;
    default:
      print("パーシャル!!! パーシャル……?");
      return;
  }
}

print("承認後はTransactionRouteから承認トランザクションの内容を取得することが可能になっている。");

var confirmedInfo = await transRouteHttp.getConfirmedInformation(aggregateTransaction.transactionHash.value);

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

実行する。

結果は以下。

{
	"meta": {
		"height": "465070",
		"hash": "DA07751937046B70B5AA487AD76A1EDFAC8B3BA7D0E5A3C00BFC5325D22BD1E5",
		"merkleComponentHash": "DA07751937046B70B5AA487AD76A1EDFAC8B3BA7D0E5A3C00BFC5325D22BD1E5",
		"index": 0,
		"timestamp": "16903106860",
		"feeMultiplier": 100
	},
	"transaction": {
		"size": 376,
		"signature": "A52BB29259A1F67F45E9035C390F42025A4187FDF299C527893C6467B757185AFB4312FC574944ECCEB78A9D68AB5F9B33B13241CD0C511DD8A26915D045250F",
		"signerPublicKey": "67626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28",
		"version": 2,
		"network": 152,
		"type": 16705,
		"maxFee": "37600",
		"deadline": "16910295000",
		"transactionsHash": "0E6D4810A4A464142A1CB2A13CCD426BAF529AB87AC2E57FF10637F385199B3E",
		"cosignatures": [],
		"transactions": [
			{
				"meta": {
					"height": "465070",
					"aggregateHash": "DA07751937046B70B5AA487AD76A1EDFAC8B3BA7D0E5A3C00BFC5325D22BD1E5",
					"aggregateId": "646224E6CF72001C11046A39",
					"index": 0,
					"timestamp": "16903106860",
					"feeMultiplier": 100
				},
				"transaction": {
					"signerPublicKey": "67626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28",
					"version": 1,
					"network": 152,
					"type": 16724,
					"recipientAddress": "9873F010CF581FAC0AAF94BC24C75F255D0FB380BD160147",
					"message": "00747831",
					"mosaics": [
						{
							"id": "72C0212E67A08BCE",
							"amount": "1"
						}
					]
				},
				"id": "646224E6CF72001C11046A3A"
			},
			{
				"meta": {
					"height": "465070",
					"aggregateHash": "DA07751937046B70B5AA487AD76A1EDFAC8B3BA7D0E5A3C00BFC5325D22BD1E5",
					"aggregateId": "646224E6CF72001C11046A39",
					"index": 1,
					"timestamp": "16903106860",
					"feeMultiplier": 100
				},
				"transaction": {
					"signerPublicKey": "67626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28",
					"version": 1,
					"network": 152,
					"type": 16724,
					"recipientAddress": "9873F010CF581FAC0AAF94BC24C75F255D0FB380BD160147",
					"message": "00747832",
					"mosaics": [
						{
							"id": "72C0212E67A08BCE",
							"amount": "2"
						}
					]
				},
				"id": "646224E6CF72001C11046A3B"
			}
		]
	},
	"id": "646224E6CF72001C11046A39"
}

transactionsの中に2つのトランザクションが確認できます。数量(amount)も1と2となっているのが確認できるかなと思います。

4.7 現場で使えるヒント

データのハッシュをブロックチェーンに刻んでおくことで、「その時に存在した証明」となる、ということのようです。

遡って改ざんや削除できないブロックチェーンだからこその利用法の一つなのでしょう。

ハッシュ値についてはそのまま速習symbolの値をコピーしてきました。

それを分割してアグリゲートトランザクションに載せます。

謎コードは以下。

// aliceのアカウントを作成する。(送信元)
var alice = await Account.createFromPrivateKey(
  PrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178E*****"), NetworkTypeEnum.testnet);

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

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

print("アグリゲートトランザクションは1024バイトで分割する");

var bigdata = 'C00200000000000093B0B985101C1BDD1BC2BF30D72F35E34265B3F381ECA464733E147A4F0A6B9353547E2E08189EF37E50D271BEB5F09B81CE5816BB34A153D2268520AF630A0A0E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26000000000198414140770200000000002A769FB40000000076B455CFAE2CCDA9C282BF8556D3E9C9C0DE18B0CBE6660ACCF86EB54AC51B33B001000000000000DB000000000000000E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26000000000198544198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F22338B000000000000000066653465353435393833444430383935303645394533424446434235313637433046394232384135344536463032413837364535303734423641303337414643414233303344383841303630353343353345354235413835323835443639434132364235343233343032364244444331443133343139464435353438323930334242453038423832304100000000006800000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D000000000198444198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F2233BC089179EBBE01A81400140035383435344434373631364336433635373237396800000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D000000000198444198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F223345ECB996EDDB9BEB1400140035383435344434373631364336433635373237390000000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D5A71EBA9C924EFA146897BE6C9BB3DACEFA26A07D687AC4A83C9B03087640E2D1DDAE952E9DDBC33312E2C8D021B4CC0435852C0756B1EBD983FCE221A981D02';

// 作りが悪く、名前が被ってしまっているらしい。
List<trans.TransactionInfo> transInfos = [];

// TransferTrans.
{

  // 送信するモザイク情報を決定する。
  List<Mosaic> mosaics = [];
  mosaics.add(Mosaic(id: MosaicId("72C0212E67A08BCE"), amount: Amount("1")));

for (var i = 0; i < bigdata.length / 1023; i++){

  // 1023で分割する。
  var startIdx = i * 1023;
  int? endIdx = startIdx + 1023;

  if (bigdata.length < endIdx) endIdx = null;

  // 送信するメッセージを構築する。
  var message = PlainMessage.create(bigdata.substring(startIdx, endIdx));

  // 送信内容を構築した。
  transInfos.add(TransferInfoV1.create(alice.publicAccount, bob.address, mosaics, message));
}

}

// Transaction設定。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(1667250467),
  fee:FeeMultiplier(100) );     // 手数料は100%。

var transWorker = TransactionWorker();

await transWorker.aggregateTransferTransaction(transSetting, transInfos);

コードはほぼ前回のを流用し、bobに複数回送ります。

aggregateTransferTransactionについては割愛。

結果は以下です。

分割して送信するだけみたいですね。

なお、ウォレットで確認した内容。

たぶん、合っていると思う。

以上です。感想編に続きます。

感想編

トランザクションのライフサイクルについて。

ロールバックの可能性については、ブロックチェーンを追いかけていればそんな話を聞いたことがあるかな、というところです。

NEM時代でも取引所から送金するのに必要な承認回数などもありました。Symbolになってからファイナライズ機構があるため、それを指標にしてもいいかもしれませんが。

※ただし、ファイナライズにはそれなりに時間がかかる。

送信したトランザクションが承認するか判定する基準は複数あって、

  • チェーン(ノード)が承認する
    トランザクションの承認が覆る可能性はあるが、一応、承認されたとすることができる。(判定が早い)
  • 承認され、対象ブロックをファイナライズが経過する
    承認されたトランザクションが覆る可能性はほぼない。

このあたりが「送信したトランザクションが正常に処理された」と認識していい指標かな? と思いました。

現状ではトランザクションがスカスカ? らしく、ロールバックされてもトランザクションは再取り込みされるようなので、若干時間/ブロックがずれるかもしれませんが、結果的にはきっと承認されるんでしょう。

トランザクションが詰まるようになってきたらファイナライズやロールバックを気にしないといけない、というところでしょうか。

アグリゲートトランザクションについて。

複数のトランザクションをまとめて実行できるというのは強力な気がします。

今回はalice -> bobを2つだったため、aliceの署名のみで済みました。
たぶんこれが、複雑になると変わっていくのだろうなぁと思います。

複数のトランザクションをひとつにまとめられることで、

  • aliceから複数のアドレスへ、複数のモザイクを一度に送信する(手数料を抑えられる?)
  • aliceからbobへ、bobからaliceへのモザイクの送信を一度で行える
  • bob、carol、アベサダオ、がそれぞれ10XYMをaliceへと送信する(誰か一人でも否認したら実行されない)
  • 現場で使えるヒントにあるように、ある程度のデータサイズであれば複数のトランザクションにまとめることで格納できる

なんてことができるのでしょうね。

複数のトランザクションを1トランザクションで実行できることで、
「複数の手続きを行う必要がある際に途中で失敗した場合、それ以降を再度実施するorそれ以前を取りやめる」
なんて選択を考える必要がなくなるのは素敵なことだなぁって。

その他。

速習Symbolは直接的には関係ないのですが、疲れました。

ベーシックトランザクションはうまくいくのに、アグリゲートトランザクションが全然うまく投げれなくてですね。SDKのペイロードと睨めっこをしていました。これ、つよつよエンジニアなんかだと簡単なのかもしれませんが。。。

元々ベーシック/アグリゲートの各トランザクションのペイロードイメージは、

目指せ北海道さんのSymbol解体新書を眺めてイメージを固めたんですよ。

その後、githubにあるSymbolSDKのcatbufferのschemaを見て「こんな……感じか……?」って手で作っていたので。

あんまりよく理解せずにやるものじゃないですね。大体ペイロードサイズと手数料計算あたりが怪しい感じでした。

一番致命的なのはアグリゲートのペイロード構築時に、[]と{}を間違えて記述している箇所でjoinするというしょうもないことをしておりまして。

どれだけやってもSDKのペイロードサイズと一致しない、、、、コードもあっているように見えるのに、、、、なぜ、、、、?

とかなってました。慣れないことをするもんじゃないね。

それでは次回はモザイク編で会いましょう。
(これ、終わるのか……?)

3. アカウント[感想編]

速習Symbolブロックチェーン体験記「とろ速Symbol」の3章、アカウントについての感想編です。

こちらでは実装ではなく、「ああ、こういうことなのかな?」といった学んだと思う部分をまとめていこうと思います。まぁ、全然わかってないんだけど。

実施編はこちら。

さて。

アカウント生成・アカウントへの送信

アカウント構成はざっくり言えば下記のイメージ。

ある意味、アカウント生成とは秘密鍵(PrivateKey)を決めること。
秘密鍵(PrivateKey)から公開鍵(PublicKey)、生アドレス(RawAddress)、アドレス(Address)の導出が可能になる。

よって、バレてはまずいのは秘密鍵。
また、秘密鍵はネットワークに晒す必要もないため、誰かに教える必要はない

上記の構成で公開アカウントで囲まれた内容がある。

これについては外部に公開可能な部分であり、SymbolノードのREST APIから直接取得できる部分でもある。

僕らが普段送信を行う対象はアドレスとなる。

また、アカウント生成時点ではネットワークが不要である。
つまり、ネットワークはアカウントが作成されたことを知らない。

つまり、秘密鍵が生成されたことをブロックチェーンは知らないため、秘密鍵から生成される公開鍵も不明であるし、アドレスも不明である。

アカウント(秘密鍵)が存在することを知らせるためには、作成したアカウントからトランザクションを発生させる必要がある。

トランザクション実行には署名が必要です。(これ、次回の話?)

署名が必要であるからには、秘密鍵が存在する。

よって、トランザクション実行により、対象のアカウントに対する公開鍵以降がチェーンに刻まれる。というお話。

なお、その場合、なぜかフォーセットから、(チェーンが認識していない)aliceアカウントに送金できるという部分がある。

おそらくこんな感じで、送信されている。

2. の秘密鍵の存在は不明~~はその通りで、よって、アドレスを間違えるとGOXの可能性がある。
(実際に導出済みのアドレスとは限らないため、本当に誰も所有していないところに飛ぶ可能性もあると思う)

そのため、アプリケーションから送金などを行う場合、通常であれば「公開鍵がブロックチェーンに登録されていること」を確認したほうが安全なのだろう。
(おそらく、あらゆる場面で)

アカウント情報の確認

先ほども書いたけれど、アカウントを作成しただけではSymbolブロックチェーンはアカウントの存在を認識していない。

また導出の順の通り、アドレスについては導出の最後になる。
そのため、アドレスから公開鍵は逆引きとなるため、不明な値(ゼロフィル)となる。

よってトランザクション実行時に公開鍵を使用するため、登録される。

うん、他には特にないですね。

現場で使えるヒント

暗号化と署名。

ここ、凄い困りました。全然暗号とかわかんないし。
AES-GCM形式っていうのはわかった。
※ざっくりいえば(aliceの秘密鍵+bobの公開鍵) = (aliceの公開鍵+bobの秘密鍵)となる鍵を作って、それで暗号化するということのようだった。

わからないのでFlutterのパッケージを組み合わせて試していたのですが、全然うまくいかなかったんですよ。これ、あれですね、わかったことではなくて愚痴ですね。

もう覚えてないのであれなんですが、ハッシュの計算? かなんかが、よく使われているのとは別らしいですね。(内容はうろ覚えですが、よく提供されているのが128bitで、こちらでは256bitだったかな?)

それについて言及しているブログを目にしたため、「あ、これパッケージじゃだめなのかも」と認識しました。
(ブログではSymbolではなく、NEMについての言及でしたけれど)

結果として中身は全然わからないのですが、tweetnacl? というソースを写経する感じに至りました。ほんまこいつ。。。。

そんなわけで、暗号化についてはこんな感じで理解しました。

これでbobはaliceのメッセージを受け取ることができるようです。

公開鍵の交換はローカルでもいいですが、トランザクションさえ発生させてしまえば公開鍵がチェーンから取得できるので、「トランザクションを発行したことがあるアカウント」であれば、秘密のメッセージを送りあうことができます。

また、他人から見た場合でも安全です。

また、「本当にaliceが送信したのか?」という疑わしい点を解消するため、署名の仕組みがあるとのことでした。

今回、速習Symbolではalice → bobへの送信したメッセージに対して、aliceが署名を行いました。

その署名内容を検証することでaliceが送ったということを証明できるようです。

なお、速習Symbolでは以下の感じです。

これ、本当はトランザクションの証明についての話でもあると思うんですが、たぶんそれは次の章で触れると思います。たぶん。

アカウントの保管

秘密鍵の暗号化をしよう! という話でした。そうですね。

以上です。お疲れさまでした。

3. アカウント[実施編]

速習Symbolブロックチェーン体験記「とろ速Symbol」です。

「1. はじめに」「2.環境構築」についてはほぼ目を通すだけのため、スキップしました。

本ページでは「アカウント」をやっていきます。

感想編はこちら。

なお、今回はこちらのテストネットノードをお借りしました。

@ftakao2007

hoge.harvesting-sweet-potatoes.club(Symbol テストネット)

(敬称略)

もちろんメインネットのノードも運用されております。

間違っていたり、問題があったら教えてください。

本稿では「3. アカウント」の実施内容を記載します。

新規作成 – 秘密鍵と公開鍵の導出 – アドレスの導出

アカウントの新規作成を行います。

秘密鍵と公開鍵のキーペアを生成し、そこからアドレスの導出を行います。

また、アドレスの導出にはテストネット or メインネットの属性が必要なようです。

アカウント新規作成

まったく当てにならない僕の自作コードはこちら。

    // 使用するホスト情報を取得する。
    var availableHost = await _getNetworkHost();

    try {
      // 新アカウントを生成する。
      var newAccount = await _generateNewAccount(availableHost);

      return newAccount;

    } catch (e) {
      debugPrint(e.toString());
      debugPrint(StackTrace.current.toString());

      throw CreateNewAccountFailedException();
    }

※全体像を載せられるようなコードになっていないため、一部分だけです。

色々ありましたが、実行結果としてはこんな感じです。

※一応秘密鍵は消しました。

秘密鍵の導出はさておいて(自分で用意していない部分でもあるので)、

導出された秘密鍵を使ってSymbolSDKから、アドレスを取得しようと思います。

そうすることで秘密鍵 → 公開鍵 → 生アドレス → アドレスの導出が同一であるということの検証とします。

※なお、raw addressと記載してますが、これをraw addressと呼んでいいかはわかりません。が、とりあえずこれで進めます。

なので比較のため、SymbolSDKでも同じことを実行します。

SymbolSDKでの実行結果。

アドレスが同一の”TCY7YADZLU6G34GK7VPGI3EUK2PMS2SDS25QMIA”となっているため、秘密鍵からアドレスの生成までは同一である、ということとしました。

アカウント情報の確認

既に(もう少し先まで)実行した関係上、データがきれいではないのですが。

実行テストを行う上で、alice、bobの秘密鍵は固定で使用していくこととします。(期間が空いた際に毎度やり直すと大変なため)

aliceのアカウント。

基本的にaliceのアカウントは上記で行います。

既にトランザクションを実行している関係上、変な値ですがxymは保持されています。

explorerで表示した残高。

こちらを取得します。

当てにならないコードはこちら。

_startProcessing();

                _accountWorker.getAccountAssetsFromAddress(Address("TAEKZ65QU3T4HK3JEEI2SOAHYNPYBUZ7BPC4YAA"))
                  .then((AccountMosaicProfile accountMosaicProfile) {
                    print("取得アカウント情報。");
                    print("address: ${accountMosaicProfile.account.address}");
                    print("public key: ${accountMosaicProfile.account.publicKey}");
                    
                    print("所持モザイク情報。");

                    var account = accountMosaicProfile.account;
                    var mosaicMap = accountMosaicProfile.mosaicInfos;

                    for (var mosaic in account.mosaics){

                      var mosaicDTO = mosaicMap[mosaic.id];
                      var mosaicAmount = "";
                      if (mosaicDTO != null && mosaicDTO.divisibility > 0){
                        var amountStr = mosaic.amount.toString();
                        var intVal = amountStr.substring(0, amountStr.length - mosaicDTO.divisibility);
                        var decVal = amountStr.substring(amountStr.length - mosaicDTO.divisibility);
                        mosaicAmount = "$intVal.$decVal";
                      }

                      print("id: ${mosaic.id} amount: $mosaicAmount");
                    }
                  })
                  .whenComplete(_finishProcessing);

実行結果はこちら。

※実行結果。

所持モザイク情報が取得できていることが確認できますね。

現場で使えるヒント

暗号化と署名

AliceとBobのみがわかるように、内容を暗号化できるみたいです。

なのでやっていきます。

当てにならないコードはこちら。


  // aliceのアカウントを作成する。
  var alice = await _accountWorker
.createNewAccountFromPrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178*****");

  // bobのアカウントを作成する。
  var bob = await _accountWorker
.createNewAccountFromPrivateKey("4A570201B9E491F0B1F4BDFE4C1AEED7D75582AAAA7771374FC88DC58C*****");

  var baseMessage = "super hogehoge";
  var message = PlainMessage.create(baseMessage);

  print("メッセージ[$message]を暗号化/復号化。");

  // メッセージの暗号化。
  var encryptedMessage = await alice.encryptMessage(message, bob.publicAccount);

  print("encrypted: ${encryptedMessage.payload}");

  // メッセージの復号化。
  // 念のため、payloadから暗号化メッセージクラスを別途インスタンス化し、復号化を行う。

  var newEncryptedMessage = await EncryptedMessage.create(encryptedMessage.payload, alice.publicAccount);
  var decryptedMessage = await bob.decryptMessage(newEncryptedMessage);

  print("decrypted: ${decryptedMessage.value}");

  print("署名と検証");

  // 復号化した内容にaliceが署名する。
  var aliceSignature = await alice.sign(decryptedMessage.toByte());

  print("aliceSignature: ${aliceSignature.value}");

  print("署名を行ったのがaliceなのかを検証する。");

  var isVerifiedAlice = await alice.publicAccount.verify(decryptedMessage, aliceSignature);
  var isVerifiedBob = await bob.publicAccount.verify(decryptedMessage, aliceSignature);

  print("alice: ${isVerifiedAlice ? "yes" : "no" }");
  print("bob: ${isVerifiedBob ? "yes" : "no"}");

“super hogehoge”をaliceがbobの公開アカウントに対して暗号化。

bobがaliceの公開アカウント情報に対して復号します。

公開アカウントってなに? っていうと「公開鍵/生アドレス/アドレス」のセットです。

※使用するのは公開鍵なんですけど。

復号した内容への署名を行い、それを検証することで、署名したのがaliceである証明とします。

実行結果はこちら。

こんな感じです。

encryptedが暗号化後のメッセージ、decryptedが復号したメッセージ。

aliceSignatureがaliceの署名です。

検証結果から、aliceが署名したことをうかがえます。

ここでの作りは僕がコーディングしただけのため、実際にSDKと同じなの? ということを確認する必要があります。

が、面倒でもあるため、SDKにて復号と署名の検証のみ行います。

ということで、復号は以下。

bobがaliceの公開情報を使用して復号した結果です。

同様に署名についても証明します。

aliceが署名していることを証明できました。

また自作のコードで暗号化 -> SDKにて復号(署名)も可能であることを確認しました。

アカウントの保管

アカウント項における最後。アカウントの保管についてです。

具体的には秘密鍵は大事な情報のため、ぱっと見ではわからないように暗号化して保管しようぜ! って話です。

使用しているaliceの秘密鍵379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178E*****を使用することとします。

ということでまずはSDKを使用して実行します。

こんな感じとなりました。

※QRコード表示はカットとします。

もちろん復号も可能です。

さて、これと同じようなものを実装しました。

※これ、厳密にはSDKに含まれているわけではない気がしますが。

そんなわけで当てにならないコードです。


  var alicePrivateKey = PrivateKey("379891A667CBA1C4F9B8DFEBCEE2AE393E4D04D162B9ED3BAB6CA17178E*****");

  print("秘密鍵の暗号化");

  // パスフレーズ付きで暗号化。
  // invest_in_kishidaはPW。
  var signerQR = qr.QRCodeGenerator.createExportAccount(alicePrivateKey, 
    NetworkTypeEnum.testnet, 
    symbolTestnetGenerationHash, "invest_in_kishida");

  var schema = signerQR.getSchema();

  var outputObject = await schema.toObject(signerQR);
  var printOutputObject = jsonEncode(outputObject);

  print("暗号結果。");
  print(printOutputObject);

  {
    print("復号する。");
    var decryptedQR = await qr.QRCodeGenerator.fromJson(printOutputObject, password: "invest_in_kishida");

    decryptedQR as qr.AccountQR;

    print("復号結果。");
    print(jsonEncode(decryptedQR.privateKey));
  }

  {
    print("速習Symbolより。Edgeを使用して作成した暗号化メッセージpayloadをdecryptする。");
    print("symbol-qr-libraryと等価であることを検証。");

    var fromBrowser = '''
      {"v":3,"type":2,"network_id":152,"chain_id":"49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4","data":{"ciphertext":"aed4e1b553e02e5a38946db98ebf795bMOiGKwS3VtiQZnIvzBRQ21IQgyAM662tKUMJ3MiI3k/+YYrGC/JVbrvtkN1WfjmLOaBXYIg3dJw6FqdZ0mh80YiOl******....","salt":"d8b4f954c878fb6888cbff896ac1409eb861706d2fd933f4ede030c515148396"}}
    ''';

    var decryptedQR = await qr.QRCodeGenerator.fromJson(fromBrowser, password: "invest_in_kishida");

    decryptedQR as qr.AccountQR;

    print("復号結果。");
    print(jsonEncode(decryptedQR.privateKey));

    // 表示するQRコードの取得。
    _dispAccountQR = await decryptedQR.toQrImage(null);
  }

実行結果はこちら。

こんな感じとなりました。

秘密鍵の暗号化 -> 復号が可能であり、SDKで暗号化した秘密鍵の復号も可能であることから、おそらく、秘密鍵の暗号化、復号が等価である。かもしれない。というところになりました。

一応、QRを表示するのも用意したんですよ。

お見せできないんですけども。

そんなわけで第3章、アカウントについては以上です。

後半の感想編に続きます。

はじめに。

説明

速習Symbolブロックチェーンを体験したことを記した記事です。

速習Symbolブロックチェーン通りにSymbolSDKを使用しようとしましたが、色々思うところあってFlutterでチャレンジしよう、ということにしました。

なお、更新頻度は遅めです。時間と、気力次第になります。

注意事項

念のため、注意事項を記しておきます。

  • 速習Symbolブロックチェーンの「はじめに」「環境構築」は飛ばします(目は通しました)。
  • 現在、作成したコードの全体を公開する予定はありません。
    • 速習Symbolブロックチェーン&Flutterを学びながらにより、遡ってリファクタリングや修正をする可能性が高いため(無計画)。
    • オレオレ構造で作っているので、誰かの当てにならないと思うので。
    • ブロックチェーンを使用しないところは、等価比較します(予定)
  • 項目の名前や理解は間違っている可能性があります。すみません。
  • 記事の更新をしながら、行き当たりばったりに学習しているため、更新頻度は低め。
    • 完走も保証はしないです。

僕が動かせて、その記録と理解を残せればいい」というスタンスで書いていきます。

注意事項は以上です。

この先は、速習Symbolを行う動機と、タイトル理由を残しておこうと思います。

速習Symbol、ご存じですか?

Symbolという名の暗号資産があります。

このSymbolという暗号資産、ブロックチェーンなのですが、難しい知識がなくても利用できる、らしいんですよ。
※もちろん、最低限の知識は必要かと思いますけども。

しかもこのSymbolっていうのは運営する中央の組織がありません。

つまり、

「勝手に使っても怒る人がいない」

そんなブロックチェーンなんですね。※たぶん、ですけど。

しかもその利用方法や機能などを速攻でお勉強させてくれるっていう神教材があるらしいんですよ。

それが

「速習Symbolブロックチェーン」

というものなんです。

Symbolは自由に使えるブロックチェーンとのことで、もともと「手弁当でも、なんか作れたら楽しそうでいいなー」と思っていました。

ほんで、そんな親切な教材があるならやってみようかな。なんて始めてみることにしました。

タイトル「とろ速Symbol」について

で、この体験記のタイトルは「とろ速Symbol」としました。

速習」言ってるのに「とろ速」ってなんで? って話でもあるんですけれども。

公式SDKはTypeScript(JavaScript)なんだよな。。。から色々あって、Flutterでやってみようか、となりました。

僕はね、基本的に「人は苦しんだことしか覚えないし、やったことしかできるようにならない」と思っているんですよ。

そんなわけで苦しむため? にFlutterでやることとしました。

僕自体はIT系の端くれエンジニアとして食ってきましたが、勉強してきたことも基本的にない、ヘボジニア(ヘボなエンジニア)です。

なのでTypeScript、JavaScriptを知らないし、Flutter(Dart)も知らないです。

もちろんブロックチェーンなんて勉強したこともありません。

インフラ系でもないですし、Web系でもないです。組み込みでもないですけど。一体何系なんだ……?

そんな僕なので、まぁ、トロトロ進むだろうと、とろ速としました。

大体の記事の構成について

1章につき、2記事を書く予定です。初めの意気込みとしては、ですが。

  • 実際にやってみた実施編
  • やってみた感想の感想編

の2本立てで行く予定です。なお、これはモチベーションに寄ります。

最後に

基本的に毎章の終わりで、投げXYM用のアドレスを公開します。

こちら!! 投げていただけると励みになります!

NCQSPV2RF5TTUMVE2TGTJ3XXAKI3FXW7WCBJHKI

.NETで構築したアプリケーションをcronで実行できなかった。

個人的な備忘録です。

具体的な目標として、

・Raspberry pi 3

・.NET6で構築したC#コンソールアプリケーション

・cronによる定期実行

ということをしたい、という状態です。

既に構築した状況として、cronのインストール済み、.NETのインストールも実施済みです。

$ dotnet アプリケーション.dll

を実行することで呼び出すことが可能なのも確認済みです。

上記のコマンドを呼び出すシェルスクリプト(hogehoge.sh)を用意し、cronを設定して実行したのですが、うまくいきませんでした。

対策を行った結果についてですが、

  1. crontab -eコマンドでcronの設定を行った。
    設定実行ユーザーが実行ユーザーとなるため、シェルのパーミッションをそれに合わせて変更した。
  2. cronの基本実行ディレクトリは、実行ユーザーのhomeとなる。
    hogehoge.shの中でcdコマンドにより、対象ディレクトリに移動した。
    ※アプリケーション.dllのパスの問題もあるし、アプリケーション上でカレントディレクトリにファイルを出力する予定であったため。
  3. cronを実行するパスにdotnetが存在していなかった。
    実行ユーザーでhogehoge.shを直接実行した場合、うまくいったのだけれど、アプリケーション.dllが実行されなかった。
    どうやら「dotnet」というコマンド自体が認識されていないようで、crontab -eの内容に、以下の内容を追加した。
PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/home/ユーザー名/.dotnet

cronの設定や、実行ディレクトリなどについては検索すると簡単に出てきてくれたのですが、dotnetを認識していない、というのは中々わかりませんでした。

というか検索しても出なかったので、予想で設定しました。

結果として実行されているので、まあよかったかな。

« Older posts

© 2023 SIXIS

Theme by Anders Noren上へ ↑