Unity
Blockchain
NEM

NEMのマルチシグトランザクション発行と署名

仮想通貨(暗号通貨)において、秘密鍵の管理は最も重要なことの1つです。
その点マルチシグは万能ではありませんが、秘密鍵を単一のアプリケーション上のみで管理する状態と比べて天と地ほどの差があるでしょう。秘密鍵の管理については色々と思うところもありますが、少し長くなるので今回はマルチシグに話題を絞ります。なお、署名数は 2of3 想定、使用している Unity のバージョンは 2017.2.0p4 、使用しているプラグインは Csharp2nem、nem-sdk です。

マルチシグが分からないという方は下記の記事を参照してください。XEM、その頃は1ドル超えてたんですね…(遠い目
nembarでNEMのマルチシグをざっくり説明する実験 - 逆襲のタヌ神

また、マルチシグが内部的にどのように処理されているかについて具体的に書かれたものだと下記の記事がとてもわかりやすいです。
NEMのマルチシグをAPIレベルで紐解く - 田舎からGeekを目指す

Unityからマルチシグ送金トランザクション発行

それでは Unity から送金トランザクションを発行してみます。使うのはいつもの Csharp2nem です。

SendMultisigTransaction.cs
using System.Collections;
using UnityEngine;
using CSharp2nem.Connectivity;
using CSharp2nem.RequestClients;
using CSharp2nem.Model.AccountSetup;
using CSharp2nem.Model.DataModels;
using CSharp2nemUtilities;

namespace NemSamples
{
    public class SendMultisigTransaction : MonoBehaviour
    {
        private Connection connection = new Connection();

        private readonly PrivateKey SignerWallet1PrivateKey = new PrivateKey("署名用アカウント1の秘密鍵");
        private readonly PrivateKey SignerWallet2PrivateKey = new PrivateKey("署名用アカウント2の秘密鍵");

        private readonly PublicKey MultisigWalletPublicKey = new PublicKey("マルチシグウォレットの公開鍵");
        private readonly Address MultisigWalletAddress = new Address("マルチシグウォレットのアドレス");

        private const string RecipientAddress = "送信先のアドレス";

        private string transactionHash = "";

        void Start()
        {
            connection.SetTestnet();
        }

        // ----- 送金 -----
        public void Send()
        {
            StartCoroutine(sendCor());
        }

        IEnumerator sendCor()
        {
            var accountFactory = new PrivateKeyAccountClientFactory(connection);
            var accClient = accountFactory.FromPrivateKey(SignerWallet1PrivateKey);

            // 1XEM送金トランザクション
            var transData = new TransferTransactionData()
            {
                Amount = 1000000,
                Message = "this is multisig send test.",
                RecipientAddress = RecipientAddress,
                ListOfMosaics = null,
                MultisigAccountKey = MultisigWalletPublicKey
            };

            var asyncResult = accClient.BeginSendTransaction(transData);

            // 送信待ち
            Debug.Log("waiting..");
            yield return new WaitUntil(() => asyncResult.IsCompleted);

            var response = accClient.EndTransaction(asyncResult);
            Debug.Log(response.ToLog());
        }

今回の場合、 Send() を呼べば送金処理が行われます。送金トランザクションでは通常の送金と異なりマルチシグウォレットの公開鍵が必要になるので注意しましょう。また、今回はテストネットなので、登場するマルチシグウォレット、署名用の2つのウォレットもテストネット用のものを使います。そのため当然 SetTestnet() しなければいけませんし、 PrivateKeyAccountClientFactory ではその connection を指定し、秘密鍵やアドレス、公開鍵もそれらに順じたものを使用するよう注意しましょう(1敗)
なお、今回は秘密鍵をベタ書きしていますが別ファイルに暗号化しておき、使用する時だけ復号して使うようにする方が良いです。

送ったリクエストは EndTransaction() で結果を受け取り、例えば response.InnerTransactionHash.Data から発行したトランザクションのトランザクションハッシュを取得することができます。
なお、async/await は Unity が正式に .NET4.6 対応していないので使っていません。Unity 2018.1 が出たら書き換えるかもしれません。

Unityでマルチシグ署名

一方で署名時には署名者の秘密鍵、マルチシグウォレットのアドレス、送金時のトランザクションハッシュの3つが必要になります。送金トランザクションを発行したクライアントが今回のようにハッシュを取得し、それを別サーバーに送って署名することもできなくはないですが、クライアントから無闇に他の署名者に対して通信を行うと悪意あるクラッカーに攻撃先を教えることになる可能性があるため、実際には他の署名者がサーバーサイドで未確認トランザクションがないかソケット通信で監視するような構成にする方が無難でしょう。ちなみに Csharp2nem はソケット通信に対応していません。また、Csharp2nem が対応していても Unity の仕様で WebGL では C# なソケット通信はできません。 JS で WebSocket 用のプラグインを自分で書く必要があるという楽しい感じになっています。これはそのうち対応する予定です。

以下の署名サンプルコードでは送信時に取得したトランザクションハッシュをそのまま署名用に流用しています。実際には上記の通りマルチシグウォレットのアドレスを元に未確認トランザクションがないか確認し、あればそのトランザクションハッシュを元に署名するようにしましょう。 加筆する余力がなかったんです…

SendMultisigTransaction.cs
        // ----- 署名 -----
        public void Cosign()
        {
            StartCoroutine(cosignCor());
        }

        IEnumerator cosignCor()
        {
            PrivateKeyAccountClientFactory accountFactory = new PrivateKeyAccountClientFactory(connection);
            PrivateKeyAccountClient accClient = accountFactory.FromPrivateKey(SignerWallet2PrivateKey);

            var sigData = new MultisigSignatureTransactionData
            {
                MultisigAddress = MultisigWalletAddress,
                TransactionHash = transactionHash
            };

            var d = accClient.EndTransaction(accClient.BeginSignatureTransactionAsync(sigData));

            yield return new WaitUntil(() => d == null);

            Debug.Log(d.ToLog());
        }
    }

}

このサンプルは Unity で署名する場合のコードですが、Unity をサーバーサイドで動かす意味はこの用途の場合皆無なのでこのコードが役に立つシチュエーションは無いと考えています。UnityではなくピュアC#をサーバーで動作させる場合は上記のコードのコルーチンを使用している部分前後を async/await を使ったものに書き換えればそのまま動くでしょう。

nem-sdkでマルチシグ署名

署名は実際にはサーバーサイドで行うことがほとんどだと思います。そして C# で利用するときと同様にどの言語でも基本的には可能です。今回は nem-sdk を用いて署名してみました。なお、 nem-sdk ではマルチシグトランザクションがカテゴライズされる unconfirmed トランザクションのソケット通信はなぜか通知が来なかったので cron を回して都度確認するようにしました。

multisig_cosign.js
let nem = require("nem-sdk").default;
let CronJob = require("cron").CronJob;

let endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultTestnet, nem.model.nodes.defaultPort);

let multisig_address = "マルチシグウォレットのウォレットアドレス";
let signer_key = "署名者の秘密鍵";

let common = nem.model.objects.create("common")("", signer_key);

// 3分に一度署名処理.
new CronJob('*/3 * * * *', () => {

    // ----- 署名 -----
    nem.com.requests.account.transactions.unconfirmed(endpoint, multisig_address).then(function(res) {

        if(res["data"].length == 0)
        {
            console.log("There is no unconfirmed transaction");
            return;
        }

        for (var i = 0; i < res["data"].length; i++) {
            let hash = res["data"][i]["meta"]["data"];
            let signatureTransaction = nem.model.objects.create("signatureTransaction")(multisig_address, hash);
            let transactionEntity = nem.model.transactions.prepare("signatureTransaction")(common, signatureTransaction, nem.model.network.data.testnet.id);
            nem.model.transactions.send(common, transactionEntity, endpoint).then(function(res) {
                console.log(res);
            });
        }

        console.log("----------");

    }, function(err) {

        console.error(err);

    });
}, null, true);


署名には通常のトランザクションをラップしているトランザクションのトランザクションハッシュが必要なので ["meta"]["data"] を取ってきています。

本当は公式も推している nem-library を使った方が良いのかもしれませんが、そちらはサンプルが乏しく辛かったので nem-sdk を使用しました。後日 nem-library 版も追記するかもしれませんが、JSを日常的に使っている方に書いていただけると嬉しいなあとか思っています。

また、こちらのコードは Heroku 等 VPS を用いて走らせることが多くなると思いますが、それだと少額とはいえ有料になるため軽い気持ちでは試しづらいです。 Google Apps Script ならこのくらいの用途であれば無料で済ませられると思い、当初そちらで書こうとしていたのですがそちらではプラグインは使えません。結果、プラグインの内部でやってくれているトランザクション発行時の処理を自分で書く必要がありめんどくさくて断念しました。これもいずれ再チャレンジするつもりですが、これまた JS を日常的に使っている方に書いていただけるとありがたいです…。

最後に注意点ですが、外部のサーバーに秘密鍵を置くのであれば、その運営元をトラストする必要があります。あの会社なら信用できるとかそんなことは全くないと思うので、実際に運用する時は 2of3 ではなく 4of5 等にして運営元が全く異なる VPS に分散する等の処置を行いましょう。トラストレスですね。トラストレス。