速習Symbolブロックチェーン4章、トランザクション編です。
……コミュニティサイトのほうにしたほうがいいのかな?
今回から、実施編と感想編を分けないことにしました。
書くのが面倒だからね。
今回はテストネットの
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のペイロードサイズと一致しない、、、、コードもあっているように見えるのに、、、、なぜ、、、、?
とかなってました。慣れないことをするもんじゃないね。
それでは次回はモザイク編で会いましょう。
(これ、終わるのか……?)