mijinを使った簡単なユーザーアクティビティログの実装例

## mijinとは？

[mijin](https://mijin.io)はテックビューロが開発したプライベートブロックチェーン製品です。
その中でmijin Catapult(v2)の説明に限定して説明したいと思います。
パブリックブロックチェーンである[Symbol](https://docs.symbol.dev/) とは同じCatapultコアエンジンであり、[Symbol](https://docs.symbol.dev/)と同等の機能があります。
他のブロックチェーン製品との大きな違いとしては、スマートコントラクトと呼ばれる機能がないですが、その代わりにスマートコントラクトに匹敵する複雑なトランザクション群を簡単に作れる**アグリゲートトランザクション機能**がコアレベルで提供されているのが特徴です。

アグリゲートトランザクション機能を使うと、例えばAさんとBさんがそれぞれ持つ二つのトークンを交換したい場合、片方ずつ送るのではなく、当人同士で一度で実現することができます。
図の例ですと、Aさんがトークンをやり取りするアグリゲートトランザクションを作成、署名し、Bさんがこのアグリゲートトランザクションに署名することでトランザクション成立し、交換処理が行われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525001/fb9b3739-c08f-b70c-7ff7-a6801465d6b7.png)

その他機能については、[公式サイト](https://mijin.io)で確認することができますが、詳しく知りたい方がおられましたら、お気軽にお問合せください。

技術的な資料に興味がありましたら、以下のデプロイメントガイドを参照頂けたらと思います。

https://aws.mijin.io/deployment/ja/index.html

## プライベートブロックチェーンを使うことのメリット

そもそもブロックチェーンを利用すると言っても、データベース製品が複数あるようにブロックチェーンにも多数の製品があり、アクセス方法が違います。
弊社製品の[mijin](https://mijin.io)というブロックチェーンは、 **RESTful API** でアクセスできるようになっており、Web開発に慣れているエンジニアであれば容易に開発することができると思います。

なぜパブリックブロックチェーンではなく、プライベートブロックチェーンを利用する必要があるの？と思われることも多いですが、ここではプライベートブロックチェーンならではの大きなメリットを3つ説明します。

* 手数料
* トランザクション量の制御
* 柔軟なカスタム設定

### 手数料

通常、ブロックチェーンの世界では、トランザクション発行(DBでいうレコードインサート)にトランザクション手数料という支払いが必須であり、増減する手数料を計算してトランザクションを発行する必要があります。
プライベートチェーンでは手数料0に設定することもできますし、手数料ありで構築した場合でも発行に使用する手数料通貨を全額管理でき、手数料の増減に悩まされる必要もありません。
そのため、とくにIoTやWebサービスのトレーサビリティログやアクティビティログなどをブロックチェーンで管理したい場合、この手数料がネックになることが多く、ブロックチェーンの不変性を利用してログだけを保存したいといったケースにおいては、パブリックブロックチェーンを使うことより費用面や管理コストが効果的です。

### トランザクション量の制御

パブリックチェーンは誰でも手数料を支払えばトランザクション発行できますが、プライベートチェーンでは手数料に使う通貨を持っているのはプライベートチェーン管理者のみですので不要なトランザクションはなく、不要なデータが突然増加しストレージが圧迫することもありません。

### 柔軟なカスタム設定

パブリックブロックチェーンでは複数のユーザーが立ち上げることからある程度安定したノードにするために、比較的上限が抑えた設定となっています。
プライベートブロックチェーンはチェーン管理者が全サーバのスペックを選定することができるため、上限を緩和した設定にすることができます。
例えば[AWS Marketplaceで提供しているmijin](https://aws.amazon.com/marketplace/seller-profile?id=953d0cd2-edc9-4162-8fc8-ef8e67b2a18b&ref=dtl_B095YR49TL)では、[Symbol](https://docs.symbol.dev/)の標準設定からいくつか任意で変更が可能になっています。

* 手数料あり/なし
* ブロック生成間隔(10秒から60秒)
* ネットワーク(mijin mijin_test)
* キャッシュサイズ
* ファイナライズタイプ(決定的、確率的)

## どのようにmijinをサービスに組み込むかを検討する

長々とmijinのご紹介させて頂きましたが、ここからが今回の本題です。
今回、弊社には[COMSA](https://comsa.io/)というNFTサービスがあるため、このサービスを使って実際にmijinにデータを保存する仕組みを検討していきます。

|検討項目|どうするか|なぜ？|
|---|---|---|
|どのようなデータを保存するか|ユーザーアクティビティログ(ニックネーム変更)|ユーザー履歴をブロックチェーン上に保存することで不変性を実現し、履歴から追うことを可能にする|
|mijinは公開するか|YES|アクティビティログを公開することで誰でもそのユーザーを検証できる。その代わりセキュリティ強化のため手数料ありモードを有効にする。|
|mijinをどこに配置するか|サービス外に置く|手数料ありで公開設定するため、とくにサービス内で管理する必要がないため|
|サービスからどのようにmijinにアクセスするか|sdkを使ってインタネット経由でアクセス|サービスとmijinは切り離して考えることで分離できる|
|mijinのユーザーアカウント管理など|サービス内部のIDから生成|ブロックチェーン特有の署名をユーザーにさせるわけでなく、あくまで識別としてアカウントを作成しログを保存すればいい|


## mijinの立ち上げと既存システムとの構成検討

mijinの立ち上げ方法は、現在二つのパターンを用意しています。

* [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=953d0cd2-edc9-4162-8fc8-ef8e67b2a18b&ref=dtl_B095YR49TL)を使用した立ち上げ
* テックビューロ社による代行構築

今回は、AWS Marketplaceを使ってmijinを立ち上げ、サービス利用する流れを説明したいと思います。
AWS Marketplaceを使ったmijin構築方法については、以下に記載していますのでここでは立ち上げ方法は割愛します。
https://aws.mijin.io/deployment/ja/deploy/aws/index.html

AWS Marketplaceを使ったmijinは、構成をカスタムできるため、以下のような図の構成としました。
あくまで既存サービス内に配置ではなく、別の領域に置くことで「組み込む」というイメージより外部APIを導入したというイメージが強いと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525001/e46c81e6-be56-9fb2-8bbf-10675e50dbc1.png)


## mijinへのログ保存方法

mijinの転送トランザクションには、メッセージと呼ばれる領域があります。
このメッセージ領域は1024byteまで書き込みができるため、ここにログ内容を記載します。

今回例ですので、ニックネーム変更ログを残すために、以下のような内容を誰でも参照できるような形でメッセージに入れます。
メッセージは暗号化して入れることもでき、特定のアカウントのみしか参照できないようにすることもできます。

```
{"activity": "change_nickname", "old":"old nickname","new":"new nickname", "timestamp": {UTC format time}}
```

### まずは簡単に書き込みの実装をする

ここでは試しにトランザクション発行をしてみます。
今現状、簡単に実装できるのは、mijin対応するためにforkしたjavascript/typescript版の[symbol-sdk](https://www.npmjs.com/package/@tech-bureau/symbol-sdk)を使用します。

:::note alert
symbol公式のsymbol-sdkでは、mijinが対応してないことに注意してください。
:::

nodejs(14又は16)を使用してsdkをインストールし、まずは簡単に実装例を示します。

sdkをインストールします。

```bash
yarn add @tech-bureau/symbol-sdk
 # または
 npm i @tech-bureau/symbol-sdk
```



```javascript
import {
  Account,
  Address,
  AggregateTransaction,
  Deadline,
  EmptyMessage,
  NetworkType,
  PlainMessage,
  RepositoryFactoryHttp,
  TransferTransaction,
} from '@tech-bureau/symbol-sdk'
import { firstValueFrom } from 'rxjs'

type ActivityMessage = {
  activity: 'change_nickname'
  old: string
  new: string
  timestamp: string
}

const main = async () => {
  // mijinに記録する履歴メッセージ
  const activity = 'change_nickname'
  const oldNickName = 'nem'
  const newNickName = 'symbol'
  const timestamp = '2021-03-16 00:06:25'
  const activityMessage: ActivityMessage = {
    activity,
    old: oldNickName,
    new: newNickName,
    timestamp,
  }

  // ----------------------------------------------------------------------------------
  // mijin設定
  // mijinのrest endpointを指定
  const nodeUrl = 'http://localhost:3000'
  // mijinのネットワークを指定
  //  NetworkType.MIJIN mijin本番
  // NetworkType.MIJIN_TEST mijinテスト
  const networkType = NetworkType.MIJIN
  // mijinのブロック1のネットワークフィンガープリント
  const generationHash = '8F2A1E5AF45AA9DAE26F180660472D6C3F12C07888C5BA19B27FC0CFC21636AD'
  // mijinの稼働日(固定値)
  const epochAdjustment = 1560294000
  // 手数料率(なしなら0、ありなら100 固定値)
  const minFeeMultiplier = 100

  // 手数料を負担する署名アカウントの秘密鍵を指定
  const signerAccountPrivateKey = ''
  const signerAccount = Account.createFromPrivateKey(signerAccountPrivateKey, networkType)

  // userアカウントの秘密鍵を指定
  const userPrivateKey = ''
  const userAccount = Account.createFromPrivateKey(userPrivateKey, networkType)
  console.log(`signer address: ${signerAccount.address.plain()}`)
  console.log(`user address: ${userAccount.address.plain()}`)


  // log記録用アドレスを指定
  const logThreadAddress = ''

  // ----------------------------------------------------------------------------------

  const repository = new RepositoryFactoryHttp(nodeUrl, { networkType, generationHash, epochAdjustment })

  // 数量0のダミートランザクションを作成
  const createDummyTransferTransaction = async (toAddress: string, message: string) => {
    const deadline = Deadline.create(epochAdjustment, 6)
    const sendRawAddress = Address.createFromRawAddress(toAddress)
    const convertedMessage = message ? PlainMessage.create(message) : EmptyMessage
    const currency = await firstValueFrom(repository.getCurrencies())
    const transferTx = TransferTransaction.create(
      deadline,
      sendRawAddress,
      [currency.currency.createRelative(0)],
      convertedMessage,
      networkType
    ).setMaxFee(minFeeMultiplier)
    return transferTx as TransferTransaction
  }

  // アグリゲートの手数料を負担するためのダミートランザクションを作成
  const initDummyTransferTransaction = await createDummyTransferTransaction(signerAccount.address.plain(), '')
  const logDummyTransferTransaction = await createDummyTransferTransaction(
    logThreadAddress,
    JSON.stringify(activityMessage)
  )
  const aggregateCompleteTransaction = AggregateTransaction.createComplete(
    Deadline.create(epochAdjustment, 6),
    [
      initDummyTransferTransaction.toAggregate(signerAccount.publicAccount),
      logDummyTransferTransaction.toAggregate(userAccount.publicAccount),
    ],
    networkType,
    []
  ).setMaxFeeForAggregate(minFeeMultiplier, 1)

  // signerとuserが署名
  const signedTransaction = signerAccount.signTransactionWithCosignatories(
    aggregateCompleteTransaction,
    [userAccount],
    generationHash
  )

  // トランザクションアナウンス
  const transactionHttp = repository.createTransactionRepository()
  await firstValueFrom(transactionHttp.announce(signedTransaction))
  console.log(`announce transaction hash: ${signedTransaction.hash}`)
}

;(async () => {
  await main().catch((err) => console.error(err))
})()
```

## 実際の構成

mijinへの繋ぎ方を確認できたことで実際のサービスへの導入を進めていきます。
COMSAのバックエンドはサーバレス構成を基本としており、AWSのいくつかのサービスを利用してmijinのエンドポイントに対してトランザクション実行を行うことにしました。
本来、SQSからLambdaに渡してそのままmijinへトランザクションを投げることもできますが、COMSAでは全て[AWS Stepfunctions](https://aws.amazon.com/jp/step-functions/)を使ったワークフローを使用しています。

理由は、
* トランザクションがなかなか承認されない可能性があった場合再送するケースがある(※1)
* トランザクションの承認チェックし、ワークフロー内で正常性確認を行える
* 再送する場合、途中のワークフローから再開できる
* 費用対効果が高い(EC2やECSを起動するよりはるかに安い)

といったブロックチェーンならではの問題をワークフローを用いることで対応しています。

※1 mijinではトランザクション承認順序がトランザクション受付順になっており、他にユーザーがいないため中々起きない問題ですが、通常パブリックチェーンは手数料を多く払うトランザクションを優先するため中々承認されないといったケースがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525001/ecb8d413-8949-6dd7-a260-44259ff32834.png)

## 導入工数

参考までに、ユーザーアクティビティログをCOMSAへ導入するまでかかった工数を示します。
mijinへの接続と、AWS使用サービス(SQS,lambda,StepFunctions)には慣れている前提の工数になります。

|フェーズ|内容|工数|
|---|---|---|
|仕様策定|どのようにmijinをサービスに組み込むかを検討する|3時間|
|インフラ策定|mijinの立ち上げと既存システムとの構成検討|30分|
|実装|mijinへのログ保存方法|2時間|
|実装|AWS Stepfunctionsを使ったワークフロー導入|2時間|
|実装|COMSA上で変更履歴 表示画面作成(デザイン含む)|8時間|
|テスト|開発環境での確認|8時間|
|本番|リリース|30分|

合計 24時間 約3人日

mijinへの接続に慣れていなかったとしても、 Webエンジニアであれば　RESTful API へのアクセスによるトランザクション発行方法は、1日もあればすんなり習得できると思います。

AWSサービスに関しては、全く慣れていない場合はそれなりの習得工数かかるかもしれませんが、おそらく数日で取得可能です。

## アクティビティログ一覧画面
[ユーザーアカウントのニックネームリンク](https://comsa.io/activity?nickname=nickname_mijin) にて、アクティビティログ一覧を表示する様にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525001/199ad61b-92d7-56e3-e204-be7363924f62.png)


## ちょっとだけ独り言

今回一番時間がかかったのは、**[COMSA上で変更履歴 表示画面作成(デザイン含む)]** でした。

**デザイン調整も、ビジネスロジックも書けるフロントエンジニア、絶賛募集中。**



## 実現できたこと

- ユーザーアクティビティログをブロックチェーン上に保存することで不変性を実現し、履歴から追うことを可能にする
- ユーザーアクティビティログを公開することで、誰でもそのユーザーを検証できる

## 最後に

既存サービスのユーザーアクティビティログを、プライベートブロックチェーン mijin へ簡単に保存する手法の解説を行いました。
より詳しい情報は以下mijin公式サイトをご覧下さい

mijin 公式サイト
https://mijin.io/

テックビューロではNFTマーケットプレイスの運営も行っております。

オールオンチェーン NFTマーケットプレイス COMSA
https://comsa.io/