[Ethereum] スマートコントラクトを呼び出してみよう(call編)
スマートコントラクトの関数を2種類に分けると、状態を変更する関数(send)と変更しない関数(call)がある。
sendやcallという呼び方は、web3.jsの web3.eth.Contract.methods
で実行するときの .send()
と .call()
を使っただけで、一般的な呼び方ではないと思う。
状態を変更する場合、ブロックチェーンのデータを更新することになるので、自分だけでなく他のトランザクションも確認して整合を取る。つまり、ブロックにトランザクションが取り込まれるまでは状態が更新されない。
状態を変更しない場合、今の状態を呼び出すだけだったり、固定の値を呼び出すだけだったりする。そういう場合は現在だけわかればよいのですぐに結果を返すことができる。
今回は、状態を変更しない場合の呼び出しをどのように行うのか見ていく。
アプリがEthereumクライアントに何かを要求する場合、web3.jsやethers.jsを使うことが多いと思う。あるいは、golangでgo-ethereumライブラリを使ったり、Javaでweb3jを使うかもしれない。
どういうライブラリを使ったとしても、Ethereumクライアントとは最終的にJSON-RPCで通信している。HTTPなのかWebsocketなのかはEthereumクライアントの設定によるが、ともかくそういうものなのだ。
初めてEthereumを扱ったときはみんなweb3.jsを使っているので、「きっとweb3.jsがEthereumプロトコルに変換しているんだ」とか思っていたのだけど、全然そういうことはなかった。
例えばblockNumberの取得は、web3.jsでは
web3.eth.getBlockNumber();
となるし、JSON-RPCであれば
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}
となる。
つまり、JSON-RPCの作り方さえ分かればcurlなどでもアクセスできるのだ。
今回は状態を変更しないスマートコントラクトの関数を、次回は状態を変更する関数を呼び出す。
状態を変更しない関数は、Solidityだと pure
かview
がついた関数になる。 pure
は状態どころか引数と関数内だけで完結する。 view
は pure
に状態の参照が加わったものだ。
view
にしていても pure
みたいに状態を参照しないこともできるのだが、solcだと “Warning: Function state mutability can be restricted to pure”と警告はしてくれるので、そんなことはしない方がよいだろう。
もちろん view
も pure
も付けずに状態を変更しない関数も書けるが、コンパイラも警告してくれることだし、そういうのはやらない方が良かろう。
では、テストする環境を作る。
truffleなどはインストールされているとして、Solidityは動けば何でもよいので以前作ったERC20サンプルを使おう。
これに pure
と view
の関数を追加する。
function funcPure(uint256 param1, uint256 param2) pure public returns(uint256) {
return 100 * (param1 + param2);
}function funcView() view public returns (string memory) {
return this.name();
}
truffleでデプロイと関数呼び出しを確認しておく。
truffle develop
truffle(develop)> migrate --reset
truffle(develop)> let inst = await OzToken.deployed()
truffle(develop)> let v = await inst.funcView()
truffle(develop)> v
'Ozln'
truffle(develop)> v = await inst.funcPure(2, 3)
truffle(develop)> v
BN {
negative: 0,
words: [ 500, <1 empty item> ],
length: 1,
red: null
}
funcView()
の方はERC20の name()
を返す。 funcPure()
は100を返す。
さて、コントラクト関数の view
や pure
を呼び出すのには、JSON-RPCでは eth_call
を使う。
optionalなものを除けば to
だけなのだが、関数呼び出しなので data
も埋める必要がある。
まず view
から。
funcView関数は引数がないので、 data
は関数のsignatureだけあればよい。
signatureは、自分でkeccak256を求めても良いし、前回のスクリプトを使ってもよいし、Visual Studio CodeにSolidity Visual Auditorをインストールしておくと、関数の上に出てくる”funcSig”をクリックすると計算してくれる。
signatureは 0xb19d7a46
だった。
to
にコントラクトアドレスを入れると、curlする内容はこうなる。
$ curl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[
{"to": "0x62338aE2638d74Fc91cF9d61d93Bcdb4e657E3DE", "data": "0xb19d7a46"}
],"id":1}' http://localhost:9545
これを実行すると、すぐに結果が返ってくる(改行は私が追加した)。
{
"id":1,
"jsonrpc":"2.0",
"result":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000044f7a6c6e00000000000000000000000000000000000000000000000000000000"
}
戻り値は、前回の関数パラメータと同じである。
まず32バイトごとに区切る。
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000004
4f7a6c6e00000000000000000000000000000000000000000000000000000000
戻り値の型はstringなので可変サイズ。つまりデータへのオフセットが最初に来て、オフセット先にまず文字サイズ、続けて文字が続く。
4文字なので 0x4f7a6c6e
までが戻り値。
UTF-8なのでこれを文字列に直すと Ozln
。先ほどtruffleで実行した値と一致する。
pure
の方もやろう。
funcPure関数のsignatureは 0xf890bda4
。
今度は引数がある。2つとも固定長なので、ビッグエンディアンの32バイトにそろえてしまえばよい。
f890bda4
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
では、curlの形式にする。
$ curl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[
{"to": "0x62338aE2638d74Fc91cF9d61d93Bcdb4e657E3DE",
"data": "0xf890bda400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003"}
],"id":1}' http://localhost:9545
これも呼び出すとすぐに結果が戻ってくる。
{"id":1,"jsonrpc":"2.0","result":"0x00000000000000000000000000000000000000000000000000000000000001f4"}
固定長で1つしか戻り値がないので、そのまま数値に変換すれば良い。
0x1f4
なので、500だ。
このように eth_call
はトランザクションの送信ではないので署名は不要である。
ではgasはどうか?
これは、パラメータに gas
や gasPrice
があるので必要となる場合があるはずだ。あるはずなのだが、私はcallの場合はgasが消費されないと思い込んでいたせいもあり、考えたことがなかった(というか、この記事を書いていて気付いた)。
ちなみに、truffleでestimateGasをすると値は出てくる。
truffle(develop)> await inst.funcPure.estimateGas(2,3)
21775
署名もしていないし、そもそも from
がないのだから請求できるアドレスがない。見えているアドレスはコントラクトアドレスだが、そこにamountがない場合も多いだろうし、この例でも入っていない。もしコントラクトアドレスから差し引くのだったとしたら、邪魔したいコントラクトを eth_call
するだけで妨害し放題になってしまう。
うーん、なんだろう。。。