Symbolモザイクの一時的な送信をゲームを題材に表現する - SecretLock&Proof Transactionの活用事例 -

# Symbolモザイクの一時的な返却を表現
<a href="https://discord.gg/KsF6qPpk" target="_Blank">とあるDiscord</a>での会話でxembookさんがこんな投稿をしていました

![スクリーンショット 2022-12-03 12.20.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/d8cb5c1f-250d-1e73-855a-000abf09c7d6.png)
この

> ゲームAで使っている間はゲームBで使えないってどうしたらいいんでしたっけ、シークレットロックかなぁ。

ここの部分をシークレットロック＆プルーフで実現する方法を本記事では紹介します。

:::note info
シークレットロック＆プルーフの簡単な説明
- モザイクの転送トランザクションを予約する。（シークレットロック）
- プルーフというシークレットロックトランザクション作成者しか知らない秘密のキーを使って予約されたトランザクションを実行する（シークレットプルーフ）
- プルーフトランザクションを実行しない限りロック時に指定したブロック数経過で返却される
:::

詳しくは以下記事群を読むことをオススメします。

https://docs.symbol.dev/ja/guides/secretlock/atomic-cross-chain-swap-between-NEM-public-and-private-chain.html

https://docs.symbol.dev/ja/concepts/cross-chain-swaps.html

https://qiita.com/planethouki/items/ad31b2228e7d12901e80

手前味噌ですが自分の記事

https://qiita.com/Toshi_ma/items/b75c82e8602d16dd59c9

これはSymbol以前のものなのでソースコードは使えませんがETHとのアトミックスワップ記事

https://qiita.com/planethouki/items/82e987bd5481c539aaa4

# ゲームを使った事例の説明

## ゲームがやりたいこと
ここにゲームA（以下DQ）とゲームB（以下FF）があります。さらにDQが発行したモザイク「メタキンソード」があります。

実はこのメタキンソードは、FFではエクスカリパーとして使えます。ただし、ゲーム制作陣としては、プレイヤーがDQでメタキンソードを装備している間に、FFでもエクスカリパーを装備できるようなお得すぎることはしたくありません。つまりどちらかで装備しているともう一方のゲームでは装備ができないようにしたいです。

なお、DQもFFもゲームアカウントはSymbolアカウントで表現されており同一です。同じく両ゲームシステムアカウントもSymbolアカウントです。（プレイヤーアカウントはゲーム毎に別でも構いませんが）

これを実現する最も簡単な方法はメタキンソードをDQシステムに送信し、使用中はDQが保有。装備を外すとプレイヤーに返却。同じくFFシステムにモザイクを送信することで装備を表現。返却で装備を外す。

![mermaid-diagram-2022-12-09-075825.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/4362cb5a-80c0-8886-fc6c-0cf9fb79ebf9.png)

## 問題と解決策
しかし、一つ問題があります（いや、問題かは微妙だけど無理やり問題としましょう）
「装備を外す」という行為をした時にゲーム側は果たしてモザイクをプレイヤーに返してくれるのでしょうか？もし、DQが秘密鍵を紛失して大切なモザイクが返却されなかったら？（本来ありえません、そんなことしたらゲームは終了です。ちなみに秘密鍵紛失を回避するためのマルチシグという機能もありますがそれはまた別の話）

ここでシークレットロック＆プルーフの出番です。「装備」を表現するためにプレイヤーはゲームアカウントにシークレットロックでモザイクを送信します。ロックされている期間は、プレイヤーの手元にもゲーム側にもありません。

装備を外す方法、それはシークレットプルーフによってゲーム運営にモザイクを一時的に渡し、すぐにプレイヤーに返却させます。

![mermaid-diagram-2022-12-09-080039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/de18c642-ac3e-38b4-aed7-737302d48969.png)

いや、それだと結局はゲームシステムが受け取るからさっきと一緒でしょ？と思ったあなた。Symbolには凄い機能があり、それが実現させてくれます。

<b>それがアグリゲートトランザクションです。</b>

https://docs.symbol.dev/ja/concepts/aggregate-transaction.html

アグリゲートトランザクションであれば、内包しているトランザクションを同時、かつ順番に処理してくれます。ここでの同時とは内包するトランザクションAは処理されるがBは処理されない、のようなことは無いという意味です。

装備を外す際のアグリゲートトランザクションの中身は
1. ゲームシステムの受取用プルーフトランザクション（署名者はプレイヤー）
1. ゲーム -> プレイヤーへの転送トランザクション（署名者はゲーム）

これらが同時に処理されるので、ゲームは大切なモザイクを所有することはありません。さらに言えば、万が一ゲーム側にそのモザイクの在庫がゼロだったとしても一時的にゲームはモザイクを手にするのでちゃんとプレイヤーに返ってきます。

プレイヤーはFFシステムにモザイクを送信し、同じようにFFにシークレットロックで装備を表現します。

## もし、ゲーム側が返却のトランザクションに署名しなかったら？

安心してください。定めたロック期間が過ぎればちゃんとプレイヤーにモザイクが返却されます。プルーフトランザクションを送信する際は必ずプレイヤーへの返却トランザクションを内包することを守れば、ゲームがモザイクを保有することはありません。

## ゲーム側はどのように装備を判断すれば良いでしょうか？

ゲームシステムはAPIを使ってシークレットロックを調べます。オーナー（ここでのオーナーとはシークレットロックの署名者）がプレイヤーかつ受取がゲーム、該当モザイクIDでステータスがUNUSED（プルーフが送信されていない）なら装備中。それ以外は装備していない。非常にシンプルでした。ロック期間が過ぎればそもそも取得できません。

さて、こんなゲームが将来現れるでしょうか？それは分かりません。ただ、この機能を使えば、一時的にモザイクを返却（渡す）ようなことが可能です。どういう用途かは思いつきません。何かのレンタルサービスでしょうか？ぜひ何か考えていただければ幸いです。

# ソースコード
Symbol SDK はv3を使います。以下記事で使用方法を紹介しています。

https://qiita.com/Toshi_ma/items/d3f91aabc57d7752f339

https://qiita.com/Toshi_ma/items/db2a81e4973274370f0d

https://qiita.com/Toshi_ma/private/0bda70e3362ad1bb8021

## シークレットロック
```js
import sdk from './sdk/javascript/src/index.js';
import fetch from 'node-fetch';
import crypto from 'crypto';

const netWork = new sdk.symbol.Network(
    'testnet',
	0x98,
	new Date(Date.UTC(2022, 9, 31, 21, 7, 47)),
	new sdk.Hash256('49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4')
)

const facade = new sdk.facade.SymbolFacade(netWork);

const playerPrivateKey = new sdk.PrivateKey('5DB8324E7EB83E7665D500B014283260EF312139034E86DFB7EE736503EAEC02');
const playerKeyPair = new sdk.symbol.KeyPair(playerPrivateKey);

const dqPublicKey = new sdk.PublicKey(sdk.utils.hexToUint8('4C4BD7F8E1E1AC61DB817089F9416A7EDC18339F06CDC851495B271533FAD13B'));
const dqAddress = facade.network.publicKeyToAddress(dqPublicKey);
const dqPlainAddress = new sdk.symbol.Address(dqAddress).toString();

const deadline = new sdk.symbol.NetworkTimestamp(facade.network.fromDatetime(Date.now())).addHours(2).timestamp;

const random = crypto.randomBytes(20);
const proof = random.toString('hex').toUpperCase();
const secret = crypto.createHash('SHA3-256').update(proof).digest('hex').toUpperCase();

console.log(`proof: ${proof}`);
console.log(`secret: ${secret}`);

const mossaicId = 0x72C0212E67A08BCEn;

const secretLockTransaction = facade.transactionFactory.create({
    type: 'secret_lock_transaction_v1',
    mosaic: { mosaicId: mossaicId, amount: 1_000000n },
    signerPublicKey: playerKeyPair.publicKey,
    duration: 5760n,
    recipientAddress: dqPlainAddress,
    secret,
    hashAlgorithm: 'sha3_256',
    deadline
});

secretLockTransaction.fee = new sdk.symbol.Amount(BigInt(secretLockTransaction.size * 100));

const playerSignature = facade.signTransaction(playerKeyPair, secretLockTransaction);
const jsonPayload = facade.transactionFactory.constructor.attachSignature(secretLockTransaction, playerSignature);

(async()=> {
    const res = await fetch("https://mikun-testnet.tk:3001/transactions", {
        method: 'put',
        body: jsonPayload  ,
        headers: {'Content-Type': 'application/json'}
    })
    console.log(await res.json());
})();
```

この時、必ずシークレットとプルーフはどこかに保存するようにしてください。プルーフを忘れてしまうと当然ながらプルーフトランザクションが送信できません（シークレットはトランザクションから取得可能です）。なお、プルーフが知られてしまうとゲームがプレイヤーの許可無しにモザイクを受け取れてしまいます。プルーフをどこかに保存する際はプレイヤーしか復元できないようプレイヤー秘密鍵で暗号化するなどが良いかもしれません。

## 返却用アグリゲートトランザクション

```js
import sdk from './sdk/javascript/src/index.js';
import fetch from 'node-fetch';

const netWork = new sdk.symbol.Network(
    'testnet',
	0x98,
	new Date(Date.UTC(2022, 9, 31, 21, 7, 47)),
	new sdk.Hash256('49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4')
)

const facade = new sdk.facade.SymbolFacade(netWork);

const playerPrivateKey = new sdk.PrivateKey('5DB8324E7EB83E7665D500B014283260EF312139034E86DFB7EE736503EAEC02');
const playerKeyPair = new sdk.symbol.KeyPair(playerPrivateKey);
const playerAddress = facade.network.publicKeyToAddress(playerKeyPair.publicKey);
const playerPlainAddress = new sdk.symbol.Address(playerAddress).toString();

const dqPrivateKey = new sdk.PrivateKey('E3839324F3CD2FC194F6E1C501D4D2CFD0DC8CCAC4307AC328E3154FF00951B9');
const dqKeyPair = new sdk.symbol.KeyPair(dqPrivateKey);
const dqAddress = facade.network.publicKeyToAddress(dqKeyPair.publicKey);
const dqPlainAddress = new sdk.symbol.Address(dqAddress).toString();

const deadline = new sdk.symbol.NetworkTimestamp(facade.network.fromDatetime(Date.now())).addHours(2).timestamp;

const mossaicId = 0x72C0212E67A08BCEn;

const secretProofTransaction = facade.transactionFactory.createEmbedded({
    type: 'secret_proof_transaction_v1',
    signerPublicKey: playerKeyPair.publicKey,
    recipientAddress: dqPlainAddress,
    secret: '61A06EC5A311909C89A9152511017F0E696460BAFDDE8D5D1588BE8E090B5997',
    hashAlgorithm: 'sha3_256',
    proof: '61FA59FDDD0A06B93F42DD84AA35ED4901244C38',
});

const returnTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: dqKeyPair.publicKey,
	recipientAddress: playerPlainAddress,
	mosaics: [
		{ mosaicId: mossaicId, amount: 1_000000n }
	]
});

const embeddedTransactions = [secretProofTransaction, returnTransaction]

const merkleHash = facade.constructor.hashEmbeddedTransactions(embeddedTransactions);

const aggreagteTransaction = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: playerKeyPair.publicKey,
    fee: 1_000000n,
    deadline,
    transactionsHash: merkleHash,
    transactions: embeddedTransactions
});

const playerSignature = facade.signTransaction(playerKeyPair, aggreagteTransaction);
facade.transactionFactory.constructor.attachSignature(aggreagteTransaction, playerSignature);

const transactionHash = facade.hashTransaction(aggreagteTransaction).bytes;
const cosignature = new sdk.symbol.Cosignature();
cosignature.version = 0n;
cosignature.signature = new sdk.symbol.Signature(dqKeyPair.sign(transactionHash).bytes);
cosignature.signerPublicKey = new sdk.symbol.PublicKey(dqKeyPair.publicKey.bytes);

aggreagteTransaction.cosignatures.push(cosignature);

const jsonPayload = `{"payload": "${sdk.utils.uint8ToHex(aggreagteTransaction.serialize())}"}`;

(async()=> {
    const res = await fetch("https://mikun-testnet.tk:3001/transactions", {
        method: 'put',
        body: jsonPayload  ,
        headers: {'Content-Type': 'application/json'}
    })
    console.log(await res.json());
})();

```

今回は、ゲーム側の秘密鍵が分かっている前提でアグリゲートコンプリートとしました。ゲームに署名（自動でも）させる場合はボンデッドを使ってください。以下にv3でのボンデッドのコードがあります。

https://qiita.com/Toshi_ma/private/0bda70e3362ad1bb8021

## ゲーム側の装備判別

https://symbol.github.io/symbol-openapi/v1.0.3/#tag/Secret-Lock-routes/operation/searchSecretLock

```js
import sdk from './sdk/javascript/src/index.js';
import fetch from 'node-fetch';

const NODE = "NODE_URL";

const getData = async (params)=> {
    const res = await fetch(NODE + params, {
        method: 'get'
    })
    return await res.json();
};

const getSecrets = async (playerAddress, gameAddress, mosaicId) => {
    const params = `/lock/secret?address=${playerAddress}`;
    const datas = await getData(params);
    const d = datas.data.filter(
        d=>{
            return d.lock.mosaicId == mosaicId 
            && playerAddress == new sdk.symbol.Address(sdk.utils.hexToUint8(d.lock.ownerAddress)) 
            && gameAddress == new sdk.symbol.Address(sdk.utils.hexToUint8(d.lock.recipientAddress)) 
            && 0 == d.lock.status
        }
    )
    if(d == undefined) return undefined;
    return d;
}

(async ()=>{
    console.log(await getSecrets("PLAYER_ADDRESS", "GAME_ADDRESS", "MOSAIC_ID") != undefined ? "装備中" : "装備していません")
})();
```

# さいごに
先程も書きましたが、アグリゲートトランザクションすげーです。

順番に処理してくれるという性質により以下のような応用も考えられるようです

https://qiita.com/nem_takanobu/items/c5509605129f6be2efde#%E5%BF%9C%E7%94%A8

Symbolではすでに検証され安全に組み込まれたスマコンを安心して使うことができます。さらにはプラグインを開発すれば自作のトランザクションを組み込むことも可能です（フォークが必要なのでノード運営者を納得させるだけのものは要る）

具体的な新しいトランザクションの開発は以下が詳しいです。

https://qiita.com/monakaJP/items/b67f9a2801be7bb72384

最後までお読みいただきありがとうございました！