アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

ganache-cliのデータを永続化する

今回はganache-cliのデータをファイルに吐き出して永続化する方法について説明します。
ganache-cliのデータを永続化できるとDappsのサンプルを提供する時にセットアップ済みのnodeもセットで提供できるため、非常に有用ですよね。
0x protocolでもganache-cliのsnapshotを提供しており、すぐにsampleを動かせるようになっています。
https://0xproject.com/wiki#Ganache-Setup-Guide0xproject.com

それでは早速、ganache-cliでデータを永続化する方法を説明します。と言っても非常に簡単だったので、さらに応用してevm_snapshotも利用した復元ポイントを持ったdumpデータの作成方法も合わせて説明したいと思います。

ganache-cliのデータをdumpする

普通に何もオプションを指定せずにganache-cliを起動した場合は終了時にデータは失われます。ganache-cliでは--dbオプションでデータの保存先を指定してから起動することで、終了後もデータを永続化することができます。

$ ganache-cli --db save/

提供されたdumpデータをロードする時も、同様に--dbオプションでデータを保持しているディレクトリを指定します。
実際にtruffleを使ってganache-cliのデータをdumpするまでの手順を説明します。

ganache-cliでデータを永続化する手順

すでにtruffleのプロジェクトを作成済みであることを前提で説明します。

1. ganache-cliのインストール

truffleプロジェクトにganache-cliを追加します。

npm i ganache-cli

2. truffle.jsにganache-cliへの接続設定を追加

いつもの通り、ganache-cliへの接続設定をtruffle.jsに追加します。

module.exports = {
  networks: {
    ganache: {
      host: "0.0.0.0",
      port: 8545,
      network_id: "*",
      gas: 3000000,
      gasPrice: 10
    }
  }
}

3. ganache-cliを--dbオプションを指定して起動

ganache-cliは別のターミナルで起動させます。--dbに渡すディレクトリはあらかじめ作成しておきましょう。
次の例ではsaveというディレクトリを作成して、そのパスを--dbに渡しています

mkdir save
node_modules/.bin/ganache-cli --db save/

4. ganache-cliにcontractをデプロイする

truffle migrateを実行し、contractをデプロイして新しいブロックを作成することでganacheの状態を更新します。

truffle migrate --network ganache

5. save/以下のデータを確認

'--db'に指定したディレクトリの中を確認するといくつかのファイルが作成されていることがわかります。

ls save
!blockHashes!0xc862bacf59accd1d3e58fdf7b7755cf160edb1f7710dc3d7babb0f66d1476b90
!blockHashes!0xc8ee11d2f9f2a039a84fb4c2c679ebb1265c70e157b69cfd1a9520cbdf2c9db5
!blockHashes!0xce3ffcdb10a5ea687e19a8234a19eeb0bfcb8de55937d209682b6244f9644255
!blockHashes!0xddbca67f007298802d478f2b9868847f8b84cca8f050329e051f9d770e067435
!blockHashes!0xeba667d98e10af0c97dca80eea9b1c68d796c3a72d8f28835a56936bb95b6d67
!blockHashes!0xee465dca15af892e2c03723c44cc5d44b72de743241a38f4b0ba7fe5be40f531
!blockLogs!0
!blockLogs!1
!blockLogs!2
!blockLogs!3
....

ganache-cliを終了してもこれらのfileは残るため、このdumpデータを提供することである時点のganache-cliの状態を他のユーザに提供することができます。

dumpデータをロードする方法は、dumpデータを作成した時と同様に、dumpデータを保存してあるディレクトリを--dbに指定してganache-cliを起動するだけです。

復元ポイントを保持したdumpデータを作成する

あらかじめセットアップ済みのganache-cliのdumpデータを作成し、提供できることはわかりました。
次はganache-cli特有のRPCであるevm_snapshotを利用して、復元ポイントを持ったdumpデータを作成する方法を説明します。
まずは、evm_snapshotとsnapshotの位置に復元するためのevm_revertについて説明します。
その後、ganache-cliの--dbと組み合わせる手順を説明します。

evm_snapshotとevm_revertについて

evm_snapshot

ganache-cliでは特殊なRPCが4つ定義されています。その中の1つであるevm_snapshotは復元ポイントを作成するコマンドです。
具体的な使い方は以下の通りです。
ここではtruffle console上で直接発行する例を示します。

web3.currentProvider.sendAsync({jsonrpc: '2.0', method: 'evm_snapshot', params: [], id: new Date().getTime()}, (err, res) => console.log(res, err))

evm_snapshotはganache-cli特有のPRCのため、web3.jsでは定義されていません。そのため直接RPCをcurrentProvider.sendAsyncで発行します。
evm_snapshotを発行すると、responseとしてsnapshot_idが返されます。このidは後述するevm_revertで必要になります。

evm_revert

evm_snapshotで作成した復元ポイントにganache-cliの状態を戻すにはevm_revertコマンドを発行します。
具体的には以下の通りです。ここでもtruffle console上で直接発行する例を示します。

web3.currentProvider.sendAsync({jsonrpc: '2.0', method: 'evm_revert', params: [<<snapshot_id>>], id: new Date().getTime()}, (err, res) => console.log(res, err))

<>にはevm_snapshotを発行したresponseとしてサーバから返されたidを指定します。snapshot_idは1から順に採番されるようです。
evm_revertの注意点としては、revertでganache-cliの状態をロールバックするとsnapshotの情報もロールバックさルことです。(考えれば当たり前ではありますが。。。)
なので、あまり複数のsnapshotを作って運用という形は適してないと思います。

ganache-cliのdumpデータにsnapshotを作成して提供する

ganache-cliのdumpのデータにsnapshotを作成した状態で提供すると、ユーザがdumpを用いてsample等を動かした後、コマンド1つでdumpロード直後のクリーンな状態に戻す。 といったことが可能になり非常に便利です。

ということで、snapshotを含めたdumpデータを提供する方法を書こうと思ったのですが、どうやらsnapshotを含めたdumpデータをロードすると、transaction発行時に下記のエラーがおきて使えませんでした。。。。

TypeError: Cannot read property 'pop' of undefined
    at CheckpointTrie.Trie._updateNode (/Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:360:24)
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:107:16
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:461:14
    at processNode (/Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:471:23)
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:457:5
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:180:7
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/util.js:75:7
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/node_modules/async/lib/async.js:52:16
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/node_modules/async/lib/async.js:269:32
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/node_modules/async/lib/async.js:44:16
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/util.js:71:7
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/merkle-patricia-tree/baseTrie.js:157:9
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/lib/database/levelupobjectadapter.js:41:16
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/level-sublevel/shell.js:101:15
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/level-sublevel/nut.js:121:19
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/encoding-down/index.js:51:21
    at /Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/node_modules/cachedown/index.js:58:21
    at ReadFileContext.callback (/Users/nakajo/work/ethereum-test/arrays-contract/node_modules/ganache-cli/node_modules/ganache-core/lib/database/filedown.js:26:14)
    at FSReqWrap.readFileAfterOpen [as oncomplete] (fs.js:420:13)

ganache-cliにもissueが上がっておりました。ひとまずはこのエラーが修正されるのを待つしかなさそうです。
TypeError: Cannot read property 'pop' of undefined when restarting from existing `--db` · Issue #600 · trufflesuite/ganache-cli · GitHub

web3を拡張してevm_snapshotを呼び出す方法

最後に余談ですが、web3.jsを拡張して、例えばweb3.eth.blockNumberのようなfunction形式でsnapshotの作成とrevertを実行できるように、web3.jsを拡張する方法を説明します。
具体的には以下のコードになります。

web3._extend({
  property: 'evm',
  methods: [new web3._extend.Method({
    name: 'snapshot',
    call: 'evm_snapshot',
    params: 0,
    outputFormatter: (val) => parseInt(val)
  })]
})
web3._extend({
  property: 'evm',
  methods: [new web3._extend.Method({
    name: 'revert',
    call: 'evm_revert',
    params: 1,
    inputFormatter: [(val) => parseInt(val)]
  })]
})

例えば、truffle testで用いる場合は次のようになります。

web3._extend({
  property: 'evm',
  methods: [new web3._extend.Method({
    name: 'snapshot',
    call: 'evm_snapshot',
    params: 0,
    outputFormatter: (val) => parseInt(val)
  })]
});

web3._extend({
  property: 'evm',
  methods: [new web3._extend.Method({
    name: 'revert',
    call: 'evm_revert',
    params: 1,
    inputFormatter: [(val) => parseInt(val)]
  })]
});

contract('TestSum', function (accounts) {
  describe("Sum Test", () => {
    it("should assert true", async () => {
      const instance = await Sum.new();
      await instance.exec([1, 2, 3])
      const result1 = await instance.result()
      assert.equal(6, result1.toNumber())
      const snapshotId = await new Promise((resolve, reject) => {
        web3.evm.snapshot((err, res) => err ? reject(err) : resolve(res))
      })

      await instance.exec([1, 2, 3, 4], {gas: 100000})
      const result2 = await instance.result()
      assert.equal(10, result2.toNumber())
      
      await new Promise((resolve, reject) => {
        web3.evm.revert(snapshotId, (err, res) => err ? reject(err) : resolve(res))
      })
      assert.equal(6, result1.toNumber())
    })
  })
})

まとめ

snapshot含めたdumpデータの提供は現状ではできませんが、それでもdumpデータを作成して提供することは非常に便利だと思います。色々なDappsプロジェクトもsampleやtutorialの説明を充実させるだけでなく、すぐに動かせるdumpデータも一緒に提供してくれると非常にありがいと思いました。