事の発端
- 暗号学やゲーム開発に精通しているわけではありません。そのため間違っている事を書いている可能性があります。おかしな点や改善点があれば指摘いただければ幸いです。(むしろそれを期待して記事にしました)
- Unityでの開発を想定しているためSybmol学習記事、速習SymbolへのリンクはC#版へ誘導しています、typescript版はオリジナルを読んでください
Solidityのチュートリアルでもあるクリプトゾンビを見てて、「あ、スマコンいいな。」と思ったのですが、これをSymbolでも実現したいなと。具体的には以下が分かりやすい。(Solidity知らなくても雰囲気で読めると思う)
function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}
}
引数はゾンビIDとターゲットIDだけで勝利の場合は敵ゾンビのDNAを与える的な。で、個人的重要ポイントはここ
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
コントラクト内でランダムな値を使い定数であるattackVictoryProbability
を超えるかどうかで勝利か敗北が決まる。
もしこのランダムな値をアプリからサーバに渡す場合はチートの可能性を否定できないし、反対にサーバ側で計算するなら運営が自由に決めることだって可能。さて、ブロックチェーンSymbolにてゲーム開発を行う身としては、どうにかこのように運営はチートを疑わなくていい。かつユーザーも運営を疑わなくていい。という状況をSymbolにて作り出せないか、ということが事の発端。
本記事では分かりやすくシンプルにするため、くじ引きアプリのような物を想定していきます。ユーザーが何かしらのコストを支払い、それに対してランダムに当選を決め、当選した場合のみ報酬を返す。と行ったものです。これを複雑にしていけば、ランダムに敵と遭遇、攻撃力を基礎値とランダム値で決定、討伐後ランダムに報酬を決める。などといったことも可能だと思います。
必要な前提知識: VRF(Verifiable Random Function)
Symbol開発者が集うDiscordにてxembookさんにVRFなるものの存在を教えていただきました(他にも相談にのっていただいたり、コメントいただいたりしました。いつもありがとうございます)
詳しくはこちらの記事が分かりやすかった
一般的なハッシュ関数と異なる点はハッシュ値の生成に非対称鍵アルゴリズムを使用していることです。つまりハッシュ値を得た第三者が、その公開鍵に対応する秘密鍵を使ってしか作り得ない値である (ハッシュ値の生成者に都合の良い値が偽造されていたり、作為的に選択されたものではない) ことを検証することができます。
Symbolアカウントは秘密鍵から生成された公開鍵とのペアで記事内verify(pk,pi) && proof_to_hash(pi) == h
の検証が可能です。
※Symbol解体新書P25あたり読むと良い
具体的な方法
参加モザイクの送信
くじ引きに参加するためにUserは必要なコストを支払います。SybmolのTransferTransactionにてSystem宛にモザイクを送信します。
TransferTransaction
Mosaicの作成
承認されたらGenerationHashを取得
Userはモザイク送信後WebSocketで自身のアドレスを監視してください。
監視
承認されたら該当のトランザクションが含まれるブロックのGenerationHashを取得します。このGenerationHashはブロックが生成されるまで誰にも知りうることができません。そのためこのハッシュ値をVRFで使用するメッセージMとします。
※参考
GHに署名してPiとHを算出
このGHに対してUserは自身のみが知りうる秘密鍵で署名し証明πを作成、かつ規定のハッシュ関数でハッシュ値Hを作成します。
例)
var pi = userKeyPair.Sign(generationHash);
var h = new byte[32];
var hasher = new Sha3Digest(256);
hasher.BlockUpdate(pi.bytes, 0, pi.bytes.Length);
hasher.DoFinal(h, 0);
TH,Pi,Hをトランザクションで送信する
Userはモザイク送信時のトランザクションハッシュ(TH),Pi,Hをメッセージに添えたTransferTransactionをSystemアドレスへ送信します。
THからモザイクが送信されているか、またPiとHを検証する
SystemはWebsocketで自身のアドレスを監視しておき、トランザクションを受信したらメッセージを確認し規定のフォーマットであれば
- THを確認、参加モザイクが送信されているのかをチェックする。
- 送られてきたTHからブロック高を取得しGHを取得
- 送信者の公開鍵を使って
verify(pk,pi) && proof_to_hash(pi) == h
を検証する
などを行い全てが正しい情報であるか検証します。
なお、こちらの情報を送信するトランザクション自体は承認は重要ではないので時間短縮のために未承認トランザクションの監視が良いかもしれません。
Hが既定値を超えていれば報酬モザイク送信
サーバ側で検証した結果を基に、Hが既定値を超えていれば当選とし報酬モザイクをTransferTransactionにて送信します。
さいごに
以上の方法であればUser・Sysytemともに公開情報を元に当選・非当選を検証できるので不正のないくじ引きが可能になるはずです。
Userは既にコストは支払っているのですが、Userが不利になるようなトランザクションの場合は署名せずトランザクションを送信しなければ良いだけなので(例えばパラメータが下がるなど。また、ゲームがクラッキングされていることも想定)、そういったケースの場合は導き出したHだけでは結果は分からずSystemの秘密鍵で署名した場合のみ結果が出るような仕組みにするのもありかもしれません。この場合はUserはSystemの公開鍵を使って結果を検証することが可能になります。
とは言え、穴はありそうなので実運用する場合にはもっとちゃんと考えなければいけないかもしれません。ツッコミお待ちしています。