ちょっと、何言ってるかわからない。
えっと、Ethereum スマートコントラクトの Hyperledger 上でのデバッグ方法です。いや、やっぱりわかりませんね。
こちらの記事「Hyperledger fabric の EVM を試す」で Hyperledger 上で EVM が動かせて、Solidity でビルドしたスマートコントラクトがデプロイできてちゃんと動作する、というのはご紹介いたしました。
ところがですね・・・デプロイ手順がめんどくさいweb3.js経由のデプロイしてしっかりブロックチェーンに書き込まれると後戻りができないのでその手前でごにょごにょしてみたいわけです。
ま、要するにデプロイ手順がめんどくさいのです!!
今回のゴール
hyperledger-fabric で EVM のチェインコード(Hyperledger流のスマートコントラクト)を実行する実際のモジュールである "fabic-chaincode-evm" のUTコードに自前のスマートコントラクトを追加してデバッグしてみる、という試みです。
対象環境
OS: Windows 10
IDE: VSCode
言語: GO 1.12.1
VSCodeはエディタですがもはやIDEでしょうということで。
対象のスマートコントラクト
実際にEVMで動くかどうか試してみたいのは前回の記事「イーサリアムのテストネットで初めてのオリジナルトークンを公開してみる」でデプロイした"マイERC20トークン"のスマートコントラクトです。OpenZeppelinをimportしたスマートコントラクトがHyperledgerで動くなら、ERC721トークンがHyperledgerに乗っかることも可能じゃないですかっ!?っていうことで、試してみたいわけです。
簡単ではございますが、こちらです。
pragma solidity ^0.5.2;
import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";
contract MyTokenERC20Sample is ERC20, ERC20Detailed {
string private _name = "Hyperledger Token";
string private _symbol = "HYP";
uint8 private _decimals = 18;
uint value = 1000000;
constructor()
ERC20Detailed(_name, _symbol, _decimals)
public
{
_mint(msg.sender, value);
}
}
_decimal
や value
が適当すぎるのはご愛嬌ということで・・・
対象プロジェクト
Hyperledger配下には多数のプロジェクトがございますが、今回のターゲットは以下です。
環境構築
それではさっそくデバッグ環境の構築を行います。
GO言語環境の構築
fabric-chaincode-evm
の開発環境であるGo言語での開発環境構築を整えます。
Go言語バージョン 1.12.1
のインストーラーを以下のダウンロードサイトからダウンロード&インストールをお願いします。
インストール後、$GOPATH/bin
をPATH
に追加しておきます。
続いてdep
を入れます。
> go get -u github.com/golang/dep/cmd/dep
プロジェクトのクローン
go get
でプロジェクトを取得します。
> go get -u github.com/hyperledger/fabric-chaincode-evm
うまくいかない場合はmkdir
で自前でフォルダ作成し、階層ずれないように注意しつつgit clone
します。
> mkdir $GOPATH/src/github.com/hyperledger
> cd $GOPATH/src/github.com/hyperledger
> git clone https://github.com/hyperledger/fabric-chaincode-evm.git
> cd fabric-chaincode-evm
つづいて依存モジュールのインストールdep
で一発です。
> dep ensure
VSCode の起動
VS Codeを起動し、Go言語用のプラグインをインストールしてください。Extentionの検索でgo
を調べるとMicrosoft提供のGoプラグインが見つかると思います。
ターゲットの階層へ移動
いよいよ確信へと迫ります。
> cd evmcc
まずは試しにターミナルから go test
と打って通常のUTが動くかどうか、確かめます。いや、動いてくれなきゃ困るけど・・・
> go test
VSCodeのデバッグ設定
続きましてVSCodeから単体テストのデバッグ起動を行うための設定をします。
デバッグ画面から ⚙ ボタンクリックによってlaunch.json
が開きます。エディタの右下のAdd Configuration...
ボタンがありがたいです。
Add Configuration...
ボタンでGo: Launch test function
を選ぶとスニペット貼り付けてくれるので、以下のように設定を変更します。
{
"name": "Launch test function",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/evmcc",
"args": ["-ginkgo.v", "-ginkgo.trace"]
},
上記のように go test
から ginkgo
にフラグなどの引数を渡したい場合は-ginkgo.v
のように記述します。
例えばログを詳細に出したい場合は以下のようにするわけです。
go test -ginkgo.v
Ginkgo について
さらっと Ginkgo についてご紹介すると GO言語での BDD ベースのテストフレームワークでございます。
他言語での BDD フレームワークに親しんでいる方は特に引っかかることもないと思います。Describe
、Context
の 2 段階構成でIt
,Expect
と書けます。
またアサーションは標準でgomega
がおすすめなようです。
以下は evmcc_test.go
の一部です。
Describe("Init", func() {
It("returns an OK response", func() {
res := evmcc.Init(stub)
Expect(res.Status).To(Equal(int32(shim.OK)))
Expect(res.Payload).To(Equal([]byte(nil)))
})
})
テストケースの作成
さて環境構築は完了です。いよいよテストケースを追加してみます。
Solidity でビルドしたバイナリの準備
いつもの通り、Remix サーバーで上記で紹介したスマートコントラクトのコードを貼り付けます。
OpenZeppelin のコードがsolidity ^0.5.2;
の縛りが入っているのでこちらもそれに合わせます。
コンパイラを0.5.2
以上のものを選択し、コンパイラのロードが終了後Ctrl+S
などでコンパイルします。
コンパイルが無事に終わったようであればBytecode
ボタンをクリックでバイナリを取得し、適当なテキストエディタに貼り付けておきます。
テストコードの追加
他のテストケースの冒頭部分を拝借して以下のようなコードを追加いたしました。
ここでは単にデプロイするだけのチェックです。
Describe("ERC20 with OpenZeppelin Dapp", func() {
var (
user0Cert = `-----BEGIN CERTIFICATE-----
MIIB/zCCAaWgAwIBAgIRAKaex32sim4PQR6kDPEPVnwwCgYIKoZIzj0EAwIwaTEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xFDASBgNVBAoTC2V4YW1wbGUuY29tMRcwFQYDVQQDEw5jYS5leGFt
cGxlLmNvbTAeFw0xNzA3MjYwNDM1MDJaFw0yNzA3MjQwNDM1MDJaMEoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp
c2NvMQ4wDAYDVQQDEwVwZWVyMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPzs
BSdIIB0GrKmKWn0N8mMfxWs2s1D6K+xvTvVJ3wUj3znNBxj+k2j2tpPuJUExt61s
KbpP3GF9/crEahpXXRajTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA
MCsGA1UdIwQkMCKAIEvLfQX685pz+rh2q5yCA7e0a/a5IGDuJVHRWfp++HThMAoG
CCqGSM49BAMCA0gAMEUCIH5H9W3tsCrti6tsN9UfY1eeTKtExf/abXhfqfVeRChk
AiEA0GxTPOXVHo0gJpMbHc9B73TL5ZfDhujoDyjb8DToWPQ=
-----END CERTIFICATE-----`
creator = marshalCreator("TestOrg", []byte(user0Cert))
deployCode = []byte("60c0604052601160808190527f48... b3068c0029")
)
BeforeEach(func() {
// Set contract creator
stub.GetCreatorReturns(creator, nil)
})
It("will create and store the runtime bytecode from the deploy bytecode and a user account", func() {
// zero address, and deploy code is contract creation
stub.GetArgsReturns([][]byte{[]byte(crypto.ZeroAddress.String()), deployCode})
res := evmcc.Invoke(stub)
Expect(res.Status).To(Equal(int32(shim.OK)))
})
})
そして最後のExpect
の箇所にブレークポイントを設定し、デバッグを走らせてみましょう。
ちゃんとブレークポイント決まりました!(よね?!)
これでSolidityのビルドバイナリを貼り付ければダイレクトに実行中のEVM(の外側)がデバッグできる環境ができました。
プロパティ(引数なしメソッド)のテストコード
このスマートコントラクトはERC20 Details
を継承していますのでsymbol
などのプロパティがあります。
これを取得してAssert
するテストコードは以下のようになります。
stub.GetArgsReturns([][]byte{[]byte(contractAddress.String()), []byte("95d89b41"})
res := evmcc.Invoke(stub)
Expect(res.Status).To(Equal(int32(shim.OK)))
// encoded bytes for "HYP"
// It consists of three elements which take byte32 each:
// - 0x32 the location of data
// - 0x3 the length of array
// - 0x728980 left-aligned 'HYP'
// Payload is "00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000372898000000000000000000000000000000000000000000000000000000000000"
Expect(string(res.Payload[64:67])).To(Equal("HYP"))
まず1行目の95d89b41
はsymbol
のメソッドがある番地です。これはRemixからスマートコントラクトを動かしてみて、symbol
の値を取得するとそのトランザクションログが流れますが、そのinput
の欄からコピーできます。
(というかそれ以外に計算方法などないのだろうか・・・??)
またSolidityは文字列の扱いが強烈に大変です。生UTF-8バイナリがbytes32に入っている、という型としてきます。なんだかよくわからないけどそんなもんです。。。
なので上記のテストケースもres.Payload[64:67]
をstring
にする、という方法でHYP
と比較しています。
Web3.js では。
Web3.js ではこのHexな文字列と元の文字列との交換は簡単で以下のように行います。
> web3.padRight(web3.fromAscii('HYP'), 34)
"0x48595000000000000000000000000000"
逆は
> web3.toAscii("0x48595000000000000000000000000000")
"HYP "
でOKです。ちょっと後半の0000...
の箇所が空白が入ってしまうのはアレですが・・・
メソッドのテストコード
引数付きのメソッドもだいたい似たようなものですが、メソッドの番地の後ろに引数のbyte値をつなげて呼び出す必要があります。これもRemixで叩いてみて、ログが流れますのでinput
欄からコピーすれば丸ごと取得できます。
実際には以下のようなコードとなります。
// 1000 -> 0x00000000000000000000000000000000000000000000000000000000000003e8
stub.GetArgsReturns([][]byte{[]byte(contractAddress.String()), []byte(transfer + hex.EncodeToString(user2Addr.Word256().Bytes()) + "00000000000000000000000000000000000000000000000000000000000003e8")})
res := evmcc.Invoke(stub)
Expect(res.Status).To(Equal(int32(shim.OK)))
stub.GetArgsReturns([][]byte{[]byte(contractAddress.String()), []byte(balanceOf + hex.EncodeToString(user2Addr.Word256().Bytes()))})
res = evmcc.Invoke(stub)
Expect(res.Status).To(Equal(int32(shim.OK)))
// 1000 -> 0x00000000000000000000000000000000000000000000000000000000000003e8
Expect(hex.EncodeToString(res.Payload)).To(Equal("00000000000000000000000000000000000000000000000000000000000003e8"))
transfer
はa9059cbb
、balanceOf
は70a08231
が割り当てられており、その後ろに引数となる値(ここではuser2Addr.Word256().Bytes()
や1000
のバイナリ値の文字列など)をくっつけて丸ごと投げています。
戻り値が数値の場合も諦めてHexな文字列で比較してしまっています。
まとめ
以下が実際に追加した箇所のテストコードになります。
SolidityのTypeのエンコードや文字列の扱い(生UTF-8のbytes32)などかなり大変ですが、リアルにスマートコントラクトが動くのがわかるので、そしてどんだけ大きくてもガス代取られないしバグっても問題ないので、トライ&エラーするには大変有効であると思います。
以上!