Unity
NEM

NEMのモザイクにオンチェーンでできるだけ情報を詰め込む

NEMでは自分だけのトークンであるモザイクを超お手軽に作ることができます。
先日、モザイクと絡めて面白いことをされている方がいました。

モザイクにシーンの情報を保存してしまおうという試みで、大変興味深いです。ぼくの方でもどのくらいの情報が保存できるのかボクセルエディタを作りつつ検証してみました。後述しますが、何も気にしないやり方では保存できる値がとても少なくなってしまうため、できる限り圧縮する方法も模索していきます。

なお、結論としてぼくの作りたいコンテンツに対してはこのやり方は合いませんでした。当然作るコンテンツによるところではありますが。また、ボクセルエディタの実装については割愛します。
圧縮の方法等も「こういう方法があるんだなあ」程度で読み飛ばしていただくのが良いかもしれません。

前提と方針

なるほど、説明文に保存するという方法を取られたようです。すごい。

リファレンスを確認すると、説明文は現時点で512文字まで保存できるとありました。

description:各概要(description)にはモザイクの説明が必要です。説明は512文字を超えることはできません。なお、説明に使用される文字に制限はありません。

https://www.pr1sm.com/crypto-coin/nem-nis-api-documentation-in-japanese/#mosaics

ただし、保存できる文字に制限こそありませんが、特殊記号は2文字として、全角文字は3文字としてカウントされるようです。
色々と模索しましたが、できるだけ多くの情報を保存するためには char 1つあたりの情報量をできるだけ多くすると良さそうでした。

そして、今回作るのはボクセルエディタのため、オブジェクトの大きさは常に同一です。そのため、保存する情報は x, y, z 座標及び色の4つとします。

情報の圧縮

特に注意せず、下記の画像のような単一のボクセルを Json として保存するとどうなるでしょうか。

Unity 2017.2.0p4 (64bit) - NEMVoxel.unity - NemSamples - PC, Mac & Linux Standalone <OpenGL 4.1> 2018-01-06 16-55-32.png

[{"x":-3.12437,"y”:0.000236,"z":-2.05621,"c":2}]

このように、1つのオブジェクトでも保存すると48文字になりました。
Jsonとして保存した場合、既存のサービスやプラグイン等での取り回しが効き、自由に値を保存できるという利点がある一方でこのように1つのオブジェクトに対して大量の文字を消費してしまいます。今回のような文字数に依存する方式の場合、両手で数えられる程度のボクセルしか置けそうにありません。
そこで、保存情報に制限をかけ自前で保存及び読み込みを行ってみます。

まずは位置情報ですが小数は保存するのをやめます。今回扱うのはボクセルであり、整数単位のみで表現には十分なためです。また、色情報は数を制限するため enum (= int) で保存します。
それぞれが int であることを前提にすれば情報量はかなり減らせそうです。その情報をクラスにすると下記のような感じになります。

    [Serializable]
    private class VoxelData
    {
        public int x, y, z;
        public VoxelConstants.Color c;

        public VoxelData(int x, int y, int z, VoxelConstants.Color c)
        {
            this.x = x;
            this.y = y;
            this.z = z;
            this.c = c;
        }
    }

次に、 int を char として保存してみましょう。
平文の場合、数字の数だけカウントされてしまいますが、 Convert.ToChar() を使用すれば最小1文字で済みます。
例えば、”934" という数字は文字列にすると3文字ですが、文字に変換すると “Φ” という1文字になります。

座標と色それぞれに1文字使用するとすれば、1つのオブジェクトに4文字消費することになり、置けるボクセルの数は 42(=512/(4 * 3)) になります。例えば下記のNEMロゴ型オブジェクトは低解像度かつ2次元の配置しか行っていませんが100個程度のボクセルからできているため、42個程度しか置けないのでは3次元の表現を行うのは大変困難、というか無理そうだなあということが予測できます。

ということで、さらに圧縮を行います。charは 1つあたり 2byte まで保存できます。2byte は 16bit です。16bit あると以下の画像の通り最大 65535 までの値( - を考慮する場合 -32768 ~ 32767)を表現することができます。

16.png

しかし、置ける数が42個なのにそれだけの範囲を表現できても大変無駄ですね。実際には1つのパラメータにつき 1byte(-128~127までの範囲)で事足りそうです。

Convert.ToByte でまず2つの int 値をそれぞれ 1byte に変換 すれば1文字に2つの値を詰め込めます。文字数制限をしたことで最大/最小値には厳しくなるためチェックも行いましょう。
次に BitConverter.ToChar(2byteからなる配列, 0) で 2byte を char に変換します。コードにすると下記のような感じです。

    char convertIntsToChar(int first, int second)
    {
        const int maxRange = 127;

        // check values
        if (first < -maxRange || first > maxRange ||
           second < -maxRange || second > maxRange)
        {
            throw new Exception("any value is out of range.");
        }

        first += 127;
        second += 127;

        var firstByte = Convert.ToByte(first);
        var secondeByte = Convert.ToByte(second);
        var chArray = new byte[] { firstByte, secondeByte };
        return BitConverter.ToChar(chArray, 0);
    }

そして char から int に戻す時は下記のようなコードになります。
先ほどと逆のことを行なっているだけですね。

    Tuple<int, int> convertCharToInts(char ch)
    {
        var bytes = BitConverter.GetBytes(ch);
        var first = Convert.ToInt16(bytes[0]);
        var second = Convert.ToInt16(bytes[1]);

        first -= 127;
        second -= 127;

        return new Tuple<int, int>(first, second);
    }

そんな感じで情報を削った結果、先ほどのボクセル1つは 罾罼 の2文字に減らして保存することが可能になりました(読めない)。
48→2なのですごい圧縮率ですね!…しかし、すこし黒魔術感があり汎用性はないし、intだけという限定的な環境を想定しているし、(詳しくないので真偽は定かではありませんが)Unicode の中には文字が割り当てられていないような箇所があるので厳しい気はします。全角文字で3文字分消費するため置ける数は結局42→85個程度ですし。

Unicode一覧 0000-0FFF - Wikipedia
https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7_0000-0FFF

メモリに乗った状態の場合、表示では空文字になっていても相互変換可能でしたが説明文に保存した場合はどうなるかは試していません。2byteを分割して保存する方式に切り替えた後空文字地帯を外したあたりの文字がヒットするようになったためです(ご存知の方はご教示いただけると大変助かります!)

ネームスペースを取得する

とりあえず検証した範囲では意図した通りの変換が行えたため、次は実際にモザイクの説明文への保存の準備を進めます。モザイクの作成にはまずネームスペースを取得する必要があります。
過去記事等を参考に、 Testnet faucet から前もって200 XEM 程取得しておいてください。

NEM の API 経由でネームスペースの取得もできるのですが、今回は汎用的な処理は必要でないため NanoWallet経由での取得を試みます。

準備ができたら NanoWallet を起動して「サービス」をクリック、ネームスペースとサブドメインより「ネームスペースを作成」を選択します。

Services — Nano Wallet 2018-01-06 15-55-46.png

ネームスペースの欄に取得したいネームスペースを入力してパスワードを入れたら登録ボタンを押下します。

Create namespace or sub-namespace — Nano Wallet 2018-01-06 15-56-52.png

作成に成功したらダッシュボードにその旨が表示されます。

612C5CA1-FACA-4F40-92AC-2F18BAFBAB5C.png

作成ができない場合、 XEM の残高が足りていないかノードが落ちている可能性があります。後者の場合、 NanoWallet 右上のノードボタンからノードを選び直すことで上手くいくかもしれません。

モザイクを作成する

CSharp2nem を用いてモザイクを作成します。
特定のネームスペースの情報を取得する場合、 NamespaceMosaicClient クラスを使用します。
また、モザイクの作成は秘密鍵を使用するため PrivateKeyAccountClient を使用します。

    public void Save()
    {
        // ボクセルデータを int に変換.
        var charList = new List<char>();
        foreach (var ve in voxelList)
        {
            charList.Add(convertIntsToChar(ve.x, ve.y));
            charList.Add(convertIntsToChar(ve.z, (int)ve.c));
        }

        StartCoroutine(saveCor(charList, "nem_voxel", "test"));
    }

    IEnumerator saveCor(List<char> charList, string nameSpace, string mosaicName)
    {
        // 指定のモザイクが存在するか確認
        NamespaceMosaicClient nmClient = new NamespaceMosaicClient(connection);
        var mosaicResult = nmClient.BeginGetMosaicsByNameSpace(nameSpace);
        yield return new WaitUntil(() => mosaicResult.IsCompleted);
        var mosaicList = nmClient.EndGetMosaicsByNameSpace(mosaicResult);
        var mosaicExists = mosaicList.Data.Any(m => m.Mosaic.Id.Name == mosaicName);

        // 既に存在する場合今回は処理を終了する
        if (mosaicExists) yield break;

        // モザイクの作成
        PrivateKeyAccountClientFactory accountFactory = new PrivateKeyAccountClientFactory(connection);
        PrivateKeyAccountClient accClient = accountFactory.FromPrivateKey(MyNemInfo.PrivateKey);

        var description = new string(charList.ToArray());

        var mosaicCreationData = new MosaicCreationData
        {
            Description = description,
            InitialSupply = 1000,
            Divisibility = 4,
            NameSpaceId = nameSpace,
            Transferable = true,
            MosaicName = mosaicName,
            SupplyMutable = true,
            MosaicLevy = null
        };

        try
        {
            accClient.BeginCreateMosaicAsync(body =>
            {
                Debug.Log(body.Content.ToLog());
            }, mosaicCreationData);
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }
    }

モザイクの説明文更新にも作成時と同じく現時点で10XEMほどの手数料がかかるため、今回は既に指定したモザイクが存在する場合は処理を中断することにしました。

ボクセルを適当に配置し、上記メソッドを実行すると下記のようにモザイクの作成に成功しました。

Dashboard — Nano Wallet 2018-01-07 19-25-07.png

モザイクから情報を読み込む

モザイクからの情報は先ほどと同様に NamespaceMosaicClient を使用して取得します。あとはモザイクの情報から説明文を取得し、コンテンツ向けの情報に変換したら完了です。今回はボクセルエディタの仕様により座標には 0.5 足しています。

    public void Load()
    {
        StartCoroutine(loadCor("nem_voxel", "test"));
    }

    IEnumerator loadCor(string nameSpace, string mosaicName)
    {
        // 指定モザイクの説明文を取得
        NamespaceMosaicClient nmClient = new NamespaceMosaicClient(connection);
        var mosaicResult = nmClient.BeginGetMosaicsByNameSpace(nameSpace);
        yield return new WaitUntil(() => mosaicResult.IsCompleted);
        var mosaicList = nmClient.EndGetMosaicsByNameSpace(mosaicResult);
        var description = mosaicList.Data.Select(m => m.Mosaic.Description).FirstOrDefault();

        // int に変換.
        var chArray = description.ToCharArray();
        for (int i = 0; i < chArray.Length - 1; i += 2)
        {
            var firstInts = convertCharToInts(chArray[i]);
            var secondInts = convertCharToInts(chArray[i + 1]);
            VoxelLocater.Instance.LocateVoxel(
                new Vector3(firstInts.Item1 + 0.5f, firstInts.Item2 + 0.5f, secondInts.Item1 + 0.5f),
                (VoxelConstants.Color)secondInts.Item2);
        }

        FindObjectOfType<VoxelCounter>().UpdateCount(chArray.Length / 2);
    }

結果がこちらで、保存時の通り読み込みできています。

Unity 2017.2.0p4 (64bit) - NEMVoxel.unity - NemSamples - PC, Mac & Linux Standalone <OpenGL 4.1> 2018-01-07 18-04-13.png

説明文への保存のメリット/デメリット

このコンテンツを作っていた時、メリットとデメリットをいくつか感じたのでそれぞれ列挙してみます(異論は是非ください)

メリット
・情報をオンチェーンで管理できる
・外部サービスを使わないのである一面では開発者の負担が減る

デメリット
・よく分からない説明文がつく
・変更にも手数料がかかる

メリット

・情報をオンチェーンで管理できる
コンテンツの情報をオンチェーンで管理できるということはブロックチェーンの恩恵を受けられるということです。つまり、改ざんは困難になり、ネームスペースの更新は必要ですが半永久的に情報が残り続けます。

・外部サービスを使わないのである一面では開発者の負担が減る
有料の外部DB等を使用する場合月額費用等は開発者の負担となります。今回の場合、作品の保存時にモザイク手数料+ネームスペース維持費の一部のXEMを手数料として送ってもらって対価としてモザイクを発行するようにすれば、開発者は実質金銭的負担なくサービスを維持できます。
ただし、今回のようにユーザーの裁量が大きい系のコンテンツを作る場合圧縮で苦心することになるかもしれません。

また、下記のツイートの方法を取ればユーザーの負担は多少増えますが、実質無限にオンチェーンで情報を保存することも可能です。

デメリット

・よく分からない説明文がつく
今回のようなちょっと特殊な圧縮のやり方では顕著ですが意味不明な説明文がつきます。あるコンテンツ専用のモザイクとして発行するのであれば問題ありませんが、そうではなく発行済みのモザイクに付加する場合そのモザイクの本来の説明は失われますし、他のコンテンツでも同様に説明文に保存したいとなっても情報を保存することはできなくなります。

・変更にも手数料がかかる
説明文の更新には現時点では作成と同じく10XEMかかります。
ほんの少し追加等の編集を行ってもそれだけかかるので結構な負担ですよね。これ自体はブロックチェーンっぽくてある意味良いとは思うのですがw

という感じです。

ぼくの作りたいものの場合、総合的に判断してデメリットがメリットを上回ってしまったため説明文は使用せずネームスペースとモザイク名に紐づけて外部DBを使用することにしようと思います。
でもオンチェーンで完結して情報を保存できるというのはとても面白いので、機会があれば積極的に利用していきたいと思います。