Symbolネットワークを介して安全にサーバーを操作する


みなさま、こんにちは。ヴィーと申します。

この記事では、Symbolブロックチェーンでのメッセージングやモザイクを利用して、外部から安全にサーバーを操作する仕組みについて考察・実験した結果を書きます。

## 目的
『私が運用しているLinuxマシンのアップデートや再起動を、見ず知らずの信頼できる第三者(自分を含むステークホルダー)にお願いしたい』
というのが動機と目的です。

これを達成するには、第三者にマシンにログイン(※)してもらうなどのセキュリティリスクの高い運用をせず、かつ確実に機能する仕組みが必要ですが、ブロックチェーン上でのメッセージングを利用することである程度は手軽に実現することができそうです。

(※ログイン) VPN構築、リモートデスクトップ、SSHキーの持出・貸出など

## 登場人物紹介
誰が何をどうしたいのか？
#### ヴィー(私)
24時間稼働のLinuxマシン君を1人で運用しているが、364泊365日の温泉旅行に行きたいと思っている。
誰かにサーバーをメンテしてほしいな、と思っているが、他人のログインとか怖いからしてほしくない。インターネット怖い。

#### サーバー
ヴィーが運用する24時間稼働のLinuxマシン君。
今回、Symbolアドレス `NDO7XR7Y4NTW7NIVRWJEZHRAISLNRLNRUNTFR2Y`を監視することになった。

#### お得意様（仮）
ヴィーのサーバーを利用しているユーザーさん。
サーバーが落ちたりバージョンアップされないと大変困るので、しっかり運用してほしいと思っている。
なんなら自分がメンテしてやりたいな、と思っている。思ってないかも。
Symbolアドレス `NDPHV22L6LVFZDKMJ4ZQ7FWVKGDJFMPNAIKPYZQ`を持っている。


## おおまかな仕組み

1. 操作権限を表すモザイク(トークン)を発行し、お得意様のSymbolアドレスに配布する。
2. サーバーは、Symbolネットワーク上の自分のアドレスに対するトランザクションを監視する。サーバーは、受信したトランザクションが操作権限トークンを持つアドレス(お得意様)からのものであれば、対応するコマンドを実行する。
3. お得意様Symbolアドレスから、サーバーSymbolアドレスに対しトランザクションを送信すると、コマンドが実行される。

## 操作権限モザイクについて
モザイクとは、Symbolネットワーク上でアカウントが自由に発行できるトークンのことです。
このトークンをサーバーの操作権限として表現し、信頼できるアカウントに付与します。

操作権限を表すため、今回作成するモザイクには以下の属性を与えて発行します。
・Revokable（召還可能）
・転送不可

Revokableなモザイクは、発行者が任意のタイミングでトークンを没収できる機能を有します。
この機能により、必要な用が済んだら速やかに権限を取り戻すことができます。

転送不可なモザイクは、発行者以外は、発行者以外へのトークンの送信ができなくなります。
権限を他のアドレスに転送されたら管理に困るため、転送不可に設定します。

モザイクの発行についてはSymbol公式ドキュメントに譲るため割愛します。
https://docs.symbolplatform.com/ja/guides/mosaic/creating-a-mosaic.html

## 具体的な実装
サーバーのnodejsで実行するコード(typescript / javascript)です。
おおまかな仕組みで説明した通り、Symbolネットワークをリスニングし、
Symbolアドレス `NDO7XR7Y4NTW7NIVRWJEZHRAISLNRLNRUNTFR2Y` に対するトランザクションを監視します。

上記アドレスに対し、操作権限を表すモザイク `1CFBC5D3ACCFD033`を数量 `100` 以上持つアドレスがトランザクションを発行した場合、サーバー側であらかじめ用意した`./great-things.sh`を起動します。

シェルスクリプトが起動されると、「(アドレス)の要求によりスクリプトが起動されました」的なメッセージを表示し、プログラムが終了します。

シェルスクリプトが起動できればこっちのものです。
目的のサービスを停止したり、アップデートしたり、再起動を行うなど…。
様々なメンテナンスが可能になるということです。


では、まずはコード全体から。
その後、それぞれのポイントを解説します。

```ts:コード全体(main.ts)
// (0) SDK等のインポート
import { Address, AggregateTransaction, PublicAccount, RepositoryFactoryHttp,
    Transaction, TransferTransaction } from 'symbol-sdk';
import { execSync } from 'child_process';

// (1) エンドポイントAPIノード
const nodeURL = 'http://angel.vistiel-arch.jp:3000';

// (2) 注文を受け付けるアドレス
const myAddress = Address.createFromRawAddress('NDO7XR7Y4NTW7NIVRWJEZHRAISLNRLNRUNTFR2Y');

// (3) 権限を表すモザイクID(トークン識別子)
const authorityMosaicId = '1CFBC5D3ACCFD033';

// (4) Symbolネットワークにアクセスするための各種SDK資材
const repositoryFactory = new RepositoryFactoryHttp(nodeURL);
const listener = repositoryFactory.createListener();
const accountHttp = repositoryFactory.createAccountRepository();

// メイン処理
(async () => {
    
    // (5) リスナーを開き、Symbolの声を聞く
    await listener.open();
    listener.newBlock().subscribe();
    console.log('Start listening Symbol Network...');

    // (6) 自分のアドレスに関連する承認Txだけを捕捉して処理
    listener.confirmed(myAddress).subscribe(

        async (confirmedTx) => {

            // (7) 送り主のアカウントを抽出
            const senderAccounts = getSenderAccounts(confirmedTx); // (7-a)
            for (const account of senderAccounts) {

                // (8) アカウントが権限を持っているかチェック
                if (await check(account.address)) { // (8-a)

                    // (9) 任意のシェルスクリプトをキック
                    const stdout = execSync(`./great-things.sh ${account.address.plain()}`);
                    console.log(stdout.toString());

                    // (10) 実行結果で後処理（ここではプログラム終了）
                    listener.close();
                    break;
                }
            }
        }
    );
})();

// (7-a) 送り主のアカウントを抽出
function getSenderAccounts(tx:Transaction) {
    var retAccounts:PublicAccount[] = [];

    if (tx instanceof AggregateTransaction) {
        for (const inner of (<AggregateTransaction>tx).innerTransactions) {
            if (inner instanceof TransferTransaction) {
                retAccounts.push(inner.signer);
            }
        }
    } else if (tx instanceof TransferTransaction) {
        retAccounts.push((<TransferTransaction>tx).signer);
    }
    return retAccounts;
}

// (8-a) アカウントが操作権限モザイクを持っているか確認する
async function check(addr: Address): Promise<Boolean> {
    const accInfo = await accountHttp.getAccountInfo(addr).toPromise();
    const mosaic = accInfo.mosaics.find((m) => m.id.toHex() === authorityMosaicId);
    return (mosaic != undefined) && (mosaic.amount.compact() >= 100);
}
```

```sh:great-things.sh
#!/bin/bash
echo "The script was executed at" $1 "request."
exit 0
```

## 解説
```.ts:(0)SDK等のインポート
import { Address, AggregateTransaction, PublicAccount, RepositoryFactoryHttp,
    Transaction, TransferTransaction } from 'symbol-sdk';
import { execSync } from 'child_process';
```
Symbolを扱うために `symbol-sdk`から各種クラスをインポートします。
SDKは`npm install symbol-sdk`でインストールします。
2行目の`child_process` はシェルスクリプトをキックしてプロセスを立ち上げるときに使います。
　


```.ts:(1)APIエンドポイントノード
const nodeURL = 'http://angel.vistiel-arch.jp:3000';
```
SymbolネットワークへアクセスするにはAPIノードへHTTP(S)アクセスします。
SDKを使ってAPIノードへアクセスすると、Symbolブロックチェーンを見たり聞いたりできます。
ちなみに上記URLは私の運用しているノードです。
　

```.ts:(2)注文を受け付けるアドレス
const myAddress = Address.createFromRawAddress('NDO7XR7Y4NTW7NIVRWJEZHRAISLNRLNRUNTFR2Y');
```
String型のSymbolアドレスを、SDKで扱えるよう、Addressクラスへと変換しています。
このアドレスに対するトランザクションを監視するのが今回の目的のひとつです。
　

```.ts:(3)権限を表すモザイクID(トークン識別子)
const authorityMosaicId = '1CFBC5D3ACCFD033';
```
モザイクにはネットワーク上で固有の識別子が割り当てられます。
今回は上記のような識別子をもつモザイクをあらかじめ発行しておきました。
これは後ほど操作権限の判定に使用します。
　

```.ts:(4)Symbolネットワークにアクセスするための各種SDK資材
const repositoryFactory = new RepositoryFactoryHttp(nodeURL);
const listener = repositoryFactory.createListener();
const accountHttp = repositoryFactory.createAccountRepository();
```
見たまんまになります。
2行目で、Symbolネットワークのブロックを聞き取るためのリスナーを生成、
3行目で、Symbolネットワークからアカウント情報を取得するためのモジュールを生成しています。
　

```.ts:(5)リスナーを開き、Symbolの声を聞く
    await listener.open();
    listener.newBlock().subscribe();
    console.log('Start listening Symbol Network...');
```
listener.open()することで、Symbolネットワークに流れるあらゆる通知を見る（聞く）ことができます。
通知は様々ですが、主にブロックの生成やファイナライズの情報などがあります。
2行目の `newBlock().subscribe()` は本来は新しく生成されたブロック情報を聴取するものですが、今回は特に何もしません。リスナーのソケットが数分で閉じてしまうのを防ぐために記述しています。
　

```.ts:(6)自分のアドレスに関連する承認Txだけを捕捉して処理
    listener.confirmed(myAddress).subscribe(
        async (confirmedTx) => {
                // (中略)
        }
    );
```
`listener.confirmed()`の引数にアドレスを指定すると、そのアドレスに関する承認されたトランザクション情報のみを受け取ることができます。
受け取った情報は `subscribe()`の中で、ここでは `confirmedTx` として受け取り、アクセスすることができます。
（ちなみに、承認されただけでファイナライズはされていないため、ロールバックする可能性はあります。）
　

```.ts:(7)送り主のアカウントを抽出
            const senderAccounts = getSenderAccounts(confirmedTx); // (7-a)
            for (const account of senderAccounts) {
                // (中略)
            }
```
```.ts:(7-a)
function getSenderAccounts(tx:Transaction) {
    var retAccounts:PublicAccount[] = [];

    if (tx instanceof AggregateTransaction) {
        for (const inner of (<AggregateTransaction>tx).innerTransactions) {
            if (inner instanceof TransferTransaction) {
                retAccounts.push(inner.signer);
            }
        }
    } else if (tx instanceof TransferTransaction) {
        retAccounts.push((<TransferTransaction>tx).signer);
    }
    return retAccounts;
}
```
結論、`senderAccounts` に送り主のアカウント情報のリストが得られます。
リスナーから受け取った`confirmedTx`は指定アドレスに関する情報の塊であり、中にはアグリゲートトランザクション(解説割愛)として複数のトランザクションが詰まっていることもあります。
この関数では、そうしたトランザクションから送信元のアカウントのみを抽出してリストで返す処理をしています。
　

```.ts:(8)アカウントが操作権限モザイクを持っているか確認する
                if (await check(account.address)) { // (8-a)
                        // (中略)
                }
```
```.ts:(8-a)
async function check(addr: Address): Promise<Boolean> {
    const accInfo = await accountHttp.getAccountInfo(addr).toPromise();
    const mosaic = accInfo.mosaics.find((m) => m.id.toHex() === authorityMosaicId);
    return (mosaic != undefined) && (mosaic.amount.compact() >= 100);
}
```
(4)で作ったモジュールを使用して、`getAccountInfo()`でアカウント情報を取得することができます。
引数にはアドレスを渡してあげます。
HTTPアクセスをするため、結果を待つためにawaitで同期を入れています。
3行目は、アカウントが持っている数多のモザイクの中から、操作権限モザイクに該当するものを抽出しています。
操作権限モザイクを持っている場合、`.amount.compact()`などとすると、持っている数量を得られます。
持っていないアカウントは undefinedを返します。
よって、この関数では、操作権限モザイクを持っていて、その数量が`100`以上の場合、trueを返します。
　

```.ts:(9)任意のシェルスクリプトをキック
                    const stdout = execSync(`./great-things.sh ${account.address.plain()}`);
                    console.log(stdout.toString());
```
```sh:great-things.sh
#!/bin/bash
echo "The script was executed at" $1 "request."
exit 0
```
ここまでをおさらいすると、サーバーに対するトランザクションを監視し、送り主が操作権限モザイクを100以上持っていることを確認してきました。
あとは、サーバー側で用意した任意のコードを実行すれば、ミッション完了です。
`execSync`はプロセスを生成してシェルでコマンドを実行します。Syncとついているので、実行結果が返るまで待ちます。
`great-things.sh`の引数にアカウントのアドレス文字列を渡して、シェルスクリプト側で表示して終える、という内容になっています。

## 試してみるの
上記のコードをサーバーにデプロイし、起動してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2237764/78ff06d9-4824-ccbf-6685-164dbffa10ca.png)
`listening Symbol network...`と表示されたら、Symbolネットワークとソケット通信している状態です。
リスナーが開いている間は、基本、プログラムは終了しません。

この状態で操作したい端末からトランザクションを送信し、サーバーに要求を出してみます。
その前にSymbolウォレットで操作側アカウントの残高を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2237764/97474f5b-0be4-3905-9411-85f1cb0aacee.png)
`saturnalia.key`というのが、操作権限を持つモザイクのエイリアスになります。
モザイクタブからIDを確認すると、IDが`1CFBC5D3ACCFD033`となっていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2237764/92ef61db-d43a-b560-90f1-e0549d9008ee.png)

では、このアドレス`NDPHV22L6LVFZDKMJ4ZQ7FWVKGDJFMPNAIKPYZQ`から、サーバー`NDO7XR7Y4NTW7NIVRWJEZHRAISLNRLNRUNTFR2Y`に対してトランザクションを送信します。送信内容はなんでも良いので、0xym メッセージなしで送信します。
（Symbolのトランザクション発行手数料は安いです。1xym 30円のレートなら、0.1円ほどで送信しても、1分～2分ほどで承認されます。いいでしょ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2237764/81d4b39d-db64-80b2-2d72-61ae09d653e6.png)

送信後、トランザクションが承認されると、サーバー側でシェルスクリプトが実行されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2237764/12752164-bbef-7699-0cc0-5f7cdee28939.png)

これで、ログインなどをせずにトランザクションひとつでLinuxを自在に操ることができそうです。

Symbolのトランザクションにはトークンの送信数量のほかにメッセージを記載したり、アグリゲートトランザクションで複数のトランザクションを組み合わせることもできるので、これらをスクリプトの実行トリガーにすれば複雑な権限を表現したり、処理の分岐などが行えると思います。

また、Symbolはブロックチェーンなので、改ざんできない操作履歴などとして残しやすくなるというメリットもあります。



## おわりに
すごく地味な内容になりましたが、ポイントは「見ず知らずの第三者に安全で確実で柔軟に権限を与える」です。
私はSymbolノードの運用をしており、それは利害を共有する第三者のユーザーさんや、ネットワーク全体にとっても重要なことなのであります。

しかし、ユーザーさんとの利害関係があるノードを24時間365日、1人で運用するのはなかなか大変です。
かといって、ブロックチェーンの向こう側にいるトラストレスな方にサーバーマシンにログインして運用を手伝ってもらうのはリスクがとても大きくなってしまいます。

そうした課題をこの方法でひとつ解決できたのは良かったと思っています。
同じようなことを他の方法でやろうとすると、規模が大きくなったり、管理が煩雑でなかなか頭を悩ませるのではないでしょうか？

余談ですが、この記事で使用したアドレスやモザイクID、ネームスペースは、現在のSymbolメインネット上で実在する私のものになります。
興味があれば覗いてみてください。あと寄付とか、分かる方は委任ハーベスティングとかしてください（宣伝）

この記事がどなたかのお役に立てることを願っています。
また、ご指摘等あればぜひお願いします。

では～