Hyperledger Fabricの初期データ登録に関し

#ブロックチェーン・アプリケーションの初期データ登録
**この記事の需要があるのかは分からない…**備忘録的に残しておく。
ブロックチェーンでchaincodeを書き、いざ初期データを登録しようとするとchaincode経由でしか登録できないことに気がついた。登録するデータが少量なら問題無いが、数万～数百万もデータがあると1件ずつ登録するには時間がかかりすぎてしまう。
当初はPostgreSQLと連携して登録するコードをGOで書いたが、ローカルだとビルドできるがchaincodeのインストール時ビルドが通らずコケてしまう。相性が悪いようだ。

そこで1つの文字列引数に可能な限りのデータを詰め込むことを考えた。つまり、

**CSVファイル→zlib(compress)で圧縮→BASE64で文字列化**

を行うツールで入力引数を作成し、chaincodeで逆を行えば良い。
このロジックでは、zlibで約1/5に圧縮されBASE64で約10%ほどサイズが増えるようだ。

#エンコードツールのコード
```GO:encode.go
package main

import (
        b64 "encoding/base64"
        "fmt"
        "flag"
        "bytes"
        "os"
        "io/ioutil"
        "compress/zlib"
)

func main() {

        // コマンドライン引数をパース
        _ = flag.Int("flag1", 0, "flag 1")
        flag.Parse()

        // CSVファイルのオープン
        f, err := os.Open(flag.Args()[0])
        if err != nil {
                fmt.Println("os.Open:error")
        }
        defer f.Close()

        // CSVファイルを読み込みzlibで圧縮
        b, err := ioutil.ReadAll(f)
        var sbuff bytes.Buffer
        zlibw := zlib.NewWriter(&sbuff)

        zlibw.Write(b)
        zlibw.Close()

        // zlibで圧縮したデータをBASE64で文字列化
        sEnc := b64.StdEncoding.EncodeToString(sbuff.Bytes())

        // 標準出力へ出力
        fmt.Println(sEnc)
}
```

使い方は、CSVファイルを引数に呼ぶだけ。結果は標準出力へ。

```bash
ls -l data.csv
-rw-r--r-- 1 root root  405488 12月 27 22:02 data.csv #元データ

./encode data.csv > data.b64z

ls -l data.b64z
-rw-r--r-- 1 root root  99525  2月  7 10:59 data.b64z #zlib圧縮+BASE64
```

#chaincodeでデコード
```GO:createAssetForB64Z
func (s *SmartContract) createAssetForB64Z(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

        fmt.Println("createAssetForB64Z:START")
        // 入力引数チェック
        if len(args) != 1 {
                return shim.Error("Incorrect number of arguments. Expecting 1")
        }

        // 入力引数の文字列をBASE64デコード
        argAsBytes, err := b64.StdEncoding.DecodeString(args[0])
        if err != nil {
                return shim.Error("b64.StdEncoding.DecodeString:error")
        }

        // BASE64デコードしたバイト列をzlibでデコード
        var sbuff bytes.Buffer
        sbuff.Write(argAsBytes)
        var dbuff bytes.Buffer
        zlibr , err := zlib.NewReader(&sbuff)
        if err != nil {
                return shim.Error("zlib.NewWriter:error")
        }

        io.Copy(&dbuff, zlibr)
        zlibr.Close()

        // CSVデータとして取り出す
        recs := csv.NewReader(strings.NewReader(string(dbuff.Bytes())))
        var asset Asset
        for {
                rec, err := recs.Read()
                if err == io.EOF {
                        break
                }
                if err != nil {
                        fmt.Println("csv.NewReader:error")
                        break
                }

                asset.Year    = rec[1]
                asset.Month   = rec[2]
                asset.Mileage, _ = strconv.Atoi(rec[3])
                asset.Battery, _ = strconv.Atoi(rec[4])
                assetAsBytes, _ := json.Marshal(asset)

                // ステートDBへPUT
                APIstub.PutState(rec[0], assetAsBytes)
        }

        fmt.Println("createAssetForB64Z:END")
        return shim.Success(nil)
}
```

#入力引数として可能なサイズとは
`xargs --show-limits`で表示できる。

```bash
xargs --show-limits
Your environment variables take up 583 bytes
POSIX upper limit on argument length (this system): 2094521
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2093938
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647
```
2,093,938バイトがMAXのように書いてあるが、実際には131,072バイトまでのようだ。
また、構築している環境が非力な場合※はchaincode処理でタイムアウトが発生するので、`split -l`コマンド等でCSVファイルを分割し調整する必要がある。AWS等のパブリッククラウドを使いXeon2CPU、4GBメモリなら131,072バイトを超えなければ問題なく実行できる。
※自宅オンプレ：Pentium+Atom混在の非力なKubernetes構成みたいな

#実際に呼び出してみる（cliを利用）
- Docker ComposeでHyperledger Fabricを構築している場合

```bash:createAssetForB64Z.sh
#!/bin/bash
files="data/*.b64z"
for file in $files; do
        echo $file
        read line < $file
        docker exec cli peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n asset -c '{"Args":["createAssetForB64Z","'${line}'"]}'
done
```

- KubernetesでHyperledger Fabricを構築している場合

```bash:createAssetForB64Z.sh
#!/bin/bash
files="data/*.b64z"
for file in $files; do
        echo $file
        read line < $file
        kubectl exec -i cli -- bash -c "peer chaincode invoke -o orderer-example-com:30050 -C mychannel -n asset -c '{\"Args\":[\"createAssetForB64Z\",\"${line}\"]}'"
done
```

#実際の効果はどうだったの？
1万件のデータを1件毎にcliバッチを使ってcreateAssetすると次の日になりそうな勢いだったのがこの方法だと、非力オンプレ・AWSの両方で数十秒のオーダーで完了した（両環境共にKubernetes上にHyperledger Fabricを構築している）。

やりたいことが実現できて満足だ。