はじめに
この記事は nem #2 Advent Calendar 2019の25日目の記事です。
昨日は @44uk_i3 さんの「かゆいところに手が届くNEM2用開発ツールを作ってる」でした。
ここでは、先日公開した「ブロックチェーン技術を利用してアイテムを交換するゲームを作った話」の技術的背景を解説していきたいと思います。
こんなゲームです
アイテム交換する様子を簡単な動画にまとめてみました。マルチシグとアグリゲートトランザクション、NEMのメリットを最大限に生かしたサンプルデモです。たったの1ファイルで動きます。 pic.twitter.com/wwvyMSkqeK
— XEMBook | NEM/mijin community (@xembook) December 23, 2019
特徴
とくにこだわった以下の部分について説明していきたいと思います。
- ブロックチェーンにおけるアイテムの所有
- アトミックなアイテムの交換
- 外部からの検証できるゲームの可能性
ブロックチェーンにおけるアイテムの所有
データの透明性が保証されるブロックチェーンにおいて「所有」とは何でしょうか?
秘匿化という特別な処理を行わない限り、ブロックチェーンでは誰もがデータの所在と関係を提示することができます。つまり、見せることができるというだけではその資産を所有したことにはなりません。その資産を「動かせること」で初めて証明することができます。資産の移動を実現するには以下のような方法が考えられます。
- 秘密鍵自体を交換する
- トークンを固有資産が紐づけられるように改造する
- アカウントに譲渡可能なアカウントを紐づけられるようにする
秘密鍵自体を交換する
この方法は推奨されません。自分の持っている秘密鍵を交換することは銀行印を交換するような行為で、秘密鍵を交換する前に複製してしまえばそれが他に存在しないといとを証明する方法がありません(おそらく誰もがバックアップを取って交換しようとするでしょう)。また、社会実装のシステムを構築する場合も開発者や運用担当者が秘密鍵を売り放すようなインセンティブを与えない設計をしておく必要があります。
トークンを固有資産が紐づけられるように改造する
NFTと呼ばれる手法ですが、これも筋のよい方法ではありません。トークンを共有して所有したい場合やアイテムの種類によって署名者を使い分けたいときなど、送金時の条件を個別に定義していくのが非常に困難になります(このあたりは詳しい方がいればご意見お聞かせください)。そもそも、トークンの一つ一つに個別情報を載せようとするのであれば、それはウォレット(アカウント)として設計すべきです。
アカウントにアカウントを紐づけられるようにする
トークンであれ固有資産であれ、所有するという役割をアカウントにだけ集中させた非常にスマートな設計です。NFTに対してNFA(Non Fungible Asset)と呼ぶ人もいます。ただし、この機能を実現させるためには譲渡可能なマルチシグ機能が必要なため、現在のところNEM以外で効率よく実装する方法がありません。
アトミックなアイテムの交換
第三者の仲介を不要とする固有資産の交換には、1ブロック単位で複数のトランザクションを処理してしまう複雑な処理が必要になります。NEM Catapultの場合はアグリゲートトランザクションを用いることでアカウントに関する状態変更、あるいは送金・入金などの複雑な組み合わせを一つのブロックに集約させることができます。私はこれを(同一チェーン上で実施する)簡便なアトミックスワップと表現しています。一つのブロックに集約させることで様々な機能を実現することができます。
- 第三者の仲介が不要
- 多彩な連署の方法
第三者の仲介が不要
一つのブロックにトランザクションを詰め込むことで、送金を確認してから入金したり本人確認が取れてから送金を行うといった複雑な業務プロセスを劇的に短縮することができます。交換途中で契約が破談になるリスクや信用のおける第三者に手続きの委託を考慮する必要がないからです。
多彩な連署の方法
Cataultではトランザクションの署名を集めるといった作業を当たり前のように活用します。同一人物の多要素認証のこともあれば、競合する同士のこともあります。また、提案者(店舗・AI)と実行者(客・ユーザー)といった構成をとることもあります。これらの署名の集め方には大きく2通りの方法があります。後述します。
外部からの検証が可能なゲーム
ゲームクリアと評価の分離
アプリケーションがゲームクリアについての証明を外部(ブロックチェーン)に出すことで、プログラムの改ざんによる不正を評価者は見抜くことができるようになります。今まではOSが提供する完全に閉鎖されたメモリ上だけでゲームを完結させる必要がありましたが、例えばブラウザ上でも同様にゲームクリアに対して厳格な評価を行うことができます。
永遠に消せないゲーム
ゲームのルールのみを定めたプログラムはネット上に公開され自由にコピーされます。たとえゲーム主催者がいなくなってもルールを引き継ぐことで誰でもゲームマスターとなりゲームを再開することができます。
処理の概要
初期設定
ユーザーアカウント「Alice」に2つのアカウントItem1とItem2を紐づけます。1-of-1 のマルチシグ構成としてAliceの秘密鍵だけでItemは自由に他のユーザーへ組み替えることができます。Itemアカウントは普通のNEMアカウントですので、今まで通り自由にモザイクトークをためたりトランザクション履歴にアポスティーユハッシュを刻んだりすることができます。
譲渡
Bobとアイテムを交換する場合を考えます。AliceからはItem2の署名権を外し、Bobの署名権を追加します。同時にItem3に対してはAliceの署名権を追加し、Bobの署名権を外します。これらが、第三者を不要な交換にするために、トランザクションを集約(アグリゲート)し、1ブロックに処理を詰め込みます。
トランザクションの集約
トランザクションの集約には2通りの方法があります。完結型(コンプリート)と保留型(ボンデッド)です。順に説明していきます。
アグリゲートトランザクション コンプリート
初期設定の場合など、全ての秘密鍵が手元にある状態でささっとマルチシグ環境を構築したい場合に便利です。マルチシグ設定されたアカウント(Item1,Item2)の秘密鍵は効力を持たないので捨ててしまって問題ありません。また応用編でオフラインなどで署名を集めたい場合にもこの方法を利用します。
Aliceが筆頭署名者となり、関係するアカウントに対して連署を促します。この間ブロックチェーンとの通信は必要ありません。nem2-sdkを利用すれば、引数に関係するアカウントクラスを指定するだけで署名が完成します。署名が集まった後、ネットワークに通知することでトランザクションがブロックに取り込まれ構成が変更されます。
アグリゲートトランザクション ボンデッド
手元ですべての秘密鍵がそろわない場合(こちらのパターンの方が多いとは思いますが)、署名がそろうまでボンデッド型としてネットワークに仮置きすることができます(手数料必要)。この方式は送金だけではなくアカウントのあらゆる状態変更に使えますので、柔軟なワンタイムスマートコントラクトを記述することが可能です。
Aliceが筆頭署名者となり、トランザクションを作成。ネットワークにロックトランザクションを通知して仮置き手数料を支払います。その後トランザクションを通知して必要な署名が集まればブロックに取り込まれます。無事にトランザクションが取り込まれれば仮置き手数料は返却されます。またnem2-sdkを利用すれば署名が必要なアカウントに対し、WebSocketでリアルタイムに通知を簡単に飛ばすことができるので便利です。
主要コードの解説
ここからはプログラムの解説を行います。
- 所有するアイテム一覧の取得
- 交換申請
- 交換申請の検知と承認
所有するアイテム一覧の取得
画面表示したアカウントが署名可能なマルチシグアカウントの一覧を取得します。
ネームスペースの取得と有効期限の取得も行うため、rxjsのzipオペレーションを用いて、同時抽出します。
//マルチシグ情報の取得
multisigHttp.getMultisigAccountInfo(alice.address)
.pipe(
ro.mergeMap(_=>_.multisigAccounts), //マルチシグのみ抽出
ro.map(_=>_.address), //アドレスのみ抽出
ro.toArray(),
ro.mergeMap(_ => {
return r.zip(
nsHttp.getAccountsNames(_), //アイテム名取得のため
nsHttp.getNamespacesFromAccounts(_) //有効期限取得のため
);
}),
)
.subscribe(x => {
var itemElem = [];
for (var i=0;i< x[0].length;i++){
$("#table").append("<tr>"
+"<td>"+ x[0][i].names[0].name + "</td>" //アイテム名
+"<td>"+ x[0][i].address.address + "</td>" // アドレス
+"<td>"+ x[1][i].endHeight.toString() + "</td>" // 有効期限
+"<td><button class='btn btn-primary exchange' type='button'>交換</button></td>"
+ "</tr>"
);
}
}, err => console.error(err));
交換申請
マルチシグの構成変更により、所有するアイテムの交換を実現します。集約アグリゲーションの仮置き型を利用し、ロックトランザクションで仮置きの申請とロックトランザクションの承認後に本体の集約トランザクションの実行も行います。
//マルチシグ変更トランザクション(譲渡)
const item1ExchangeTx = nem.MultisigAccountModificationTransaction.create(
nem.Deadline.create(),
0,0,
[ _[0].publicAccount ],
[ alice.publicAccount ],
nem.NetworkType.TEST_NET
);
//マルチシグ変更トランザクション(受け取り)
const item2ExchangeTx = nem.MultisigAccountModificationTransaction.create(
nem.Deadline.create(),
0,0,
[ alice.publicAccount ],
[ _[0].publicAccount ],
nem.NetworkType.TEST_NET
);
//集約トランザクション
const aggregateTx = nem.AggregateTransaction.createBonded(
nem.Deadline.create(),
[
item1ExchangeTx.toAggregate(_[2].publicAccount),
item2ExchangeTx.toAggregate(_[1].publicAccount),
],
nem.NetworkType.TEST_NET,
[],
nem.UInt64.fromUint(1000000)
);
//署名
const signedTx = alice.sign(aggregateTx, GENERATION_HASH);
//ロックトランザクション
const lockTx = nem.HashLockTransaction.create(
nem.Deadline.create(),
new nem.Mosaic(
new nem.MosaicId('75AF035421401EF0'),
nem.UInt64.fromUint(10000000)
),
nem.UInt64.fromUint(480),
signedTx,
nem.NetworkType.TEST_NET,
nem.UInt64.fromUint(100000)
);
const lockSignedTx = alice.sign(lockTx, GENERATION_HASH);
//ロックトランザクションをネットワークにアナウンス
txHttp
.announce(lockSignedTx)
.subscribe(x => {
}, err => console.error(err));
//ロックトランザクションの検知と本体トランザクションのアナウンス
listener.confirmed(alice.address)
.pipe(
ro.filter((tx) => tx.transactionInfo !== undefined && tx.transactionInfo.hash === lockSignedTx.hash),
ro.mergeMap(ignored => txHttp.announceAggregateBonded(signedTx))//本体のアナウンス
)
.subscribe(
_ => {},
err => console.error(err)
);
交換申請の検知
自分の画面を開いたときに、他のユーザから交換申請が来ていないかをチェックします。
存在していた場合はネームスペース情報を取得し、ダイアログに表示します。
後半は承認部分のロジックです。rxjsのpipeの中で署名とアナウンスをこなしてしまうのが非常にトリッキーです。
//申請検知と画面表示
accountHttp.getAccountPartialTransactions(alice.address)
.pipe(
ro.mergeMap(_ => _),
ro.filter((_) => !_.signedByAccount(alice.publicAccount)),
)
.subscribe(_ =>{
nsHttp.getAccountsNames([
_.innerTransactions[0].signer.address,
_.innerTransactions[1].signer.address,
_.innerTransactions[0].publicKeyDeletions[0].address
])
.subscribe(
_=>{
$("#exuser" ).val(_[2].names[0].name);
$("#getitem" ).val(_[0].names[0].name);
$("#lostitem").val(_[1].names[0].name);
$('#myModal4').modal('show');
}
)
}, err => console.error(err));
//承認(確認ボタンクリック時に処理)
accountHttp.getAccountPartialTransactions(alice.address)
.pipe(
ro.mergeMap(_ => _),
ro.filter((_) => !_.signedByAccount(alice.publicAccount)), //署名済みのトランザクションを除外
ro.map(_ => alice.signCosignatureTransaction(nem.CosignatureTransaction.create(_)), // 署名
ro.mergeMap(_ => txHttp.announceAggregateBondedCosignature(_)) //ネットワークにアナウンス
)
.subscribe(_ => {},
err => console.error(err)
);
ソースコード
リファクタリングが済んでいませんが何かの参考になりましたら。
https://github.com/xembook/nem-tech-book/blob/master/501_exchange_item.html
おわりに
2年ほど譲渡可能なマルチシグの重要性を説明してきましたが、アイテム交換もこの概念が理解できて初めて実現できるものです。ゲームではアイテムという名称を使いましたが、実社会において本当に交換させたいのは鍵であったり、証明済みの証です。ブロックチェーンはマルチシグが当たり前のように使えて初めて成長することができます。このゲームに関する設計は私が初めてCatapultの仕様を聞いた時から構想を深め、今回のnem アドベントカレンダーである程度の完成を見ることができました。今回の投稿を読んだみなさんが、ご自身の業務に置き換えられてどんどん活用されていくことを願っています。