##[はじめに]
昨年購入したESP-WROOM-02(ESP8266)も、wifiに繋がったことを確認して満足していたので埃をかぶってしまいました。
電子工作が好きな人の界隈では、人気商品だったのを私は覚えています。
それが、いまの状況を見ると、どうもやるせない気持ちになります。
「たしか、これでwifiつないで、公開されているAPIにjson投げて、データでもとれないかな・・・」と思案していたころが懐かしいです。
遠く車が行き交う車道を眺めながら、お茶を手にしたところで「それって、コントラクトにアクセスできるってことなんじゃ・・・」という発想がよぎります。
大変です。もう頭の中がEthereumのことでいっぱいです。
##[概要]
- ESP8266をWIFIに接続し、プライベートチェーンのEthereumノードにアクセスします。
- プライベートネットワークに接続しているノードにRPC-APIでコントラクトの参照・実行を行います。
- 今回はweb3が使えません。コントラクトの実行や要求はパラメータを16進数にエンコードします。
##[準備するもの]
「Wi-Fiモジュール ESP-WROOM-02 DIP化キット」
http://akizukidenshi.com/catalog/g/gK-09758/
この記事の主役です。
これで回路を組んでしまえばWifiを搭載したAruduinoっぽいことができてしまう代物です。650円分の価値は十分あります。
回路については、「hiramine.com」さんのページに詳しく丁寧に書かれていますので、実際に回路を組んでみようと思う方は、それを参考にされると良いと思います。
http://www.hiramine.com/physicalcomputing/espwroom02/index.html
接続先のEthereumノード
前回立てたプライベートネットワークのノードに接続します。
※今回は-rpcaddr "192.168.100.100"に設定しています。(前回はlocalhost)
$geth --datadir private_net -networkid 65000 --rpc "192.168.100.100" --rpcport "8545" --nodiscover --rpcapi eth,web3,personal console
##[環境]
Arduinoのスケッチを描くのはubuntuでもできますが、後述するesp8266のライブラリを使用するにはあまり具合がよくないので、今回はWindows7で作成します
##[Step1.Ethereumノードのブロック番号を取得する]
ESP8266はWifiとの通信が複雑なので、 githubに公開されているesp8266のライブラリを使用します。
https://github.com/esp8266/Arduino
これによってESP8266はAruduinoライクにスケッチを描くことで動作させることができます。
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
void setup()
{
Serial.begin(115200);
Serial.println();
WiFi.begin("MYSSID", "PASSWORD");
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
}
void http_post(){
HTTPClient http;
http.begin("http://192.168.100.100:8545/");
http.addHeader("Content-Type", "application/json");
String payload ="{\"method\":\"eth_blockNumber\",\"params\":{},\"id\":0}";
Serial.println(payload);
int httpCode = http.POST(payload);
void loop() {
http_post();
delay(5000);
}
これでEthereumノードにアクセスできます。
コード上に記載したhttp_post()部分がjsonをノードに向けてPOSTします。
”payload”の記述がEthereumのjson-rpcによるapiの仕様を満たしていれば、きちんとした応答があるはずです。
json-rpcの仕様はここに書いてあります。
https://github.com/ethereum/wiki/wiki/JSON-RPC
##BlockNumberの取得
ここでは、プライベートネットワーク上のblockNumberを返却する要求を出しています。
String payload ="{\"method\":\"eth_blockNumber\",\"params\":{},\"id\":0}";
結果がでてきました。
["jsonrpc":2.0","id":0,"result":0x8ab]
resultの0x8abは16進数なので10進数に直すと2219ですね。
プライベートネットワークのノードのコンソールを開いてブロックナンバーを確認します。
>eth.blockNumber
2219
一致しているのが確認できました。
##[Step2.コントラクトにアクセスする]
次にコントラクトにアクセスします。実行するコントラクトは次のようにしました。
privatenetwork上に展開されてるコントラクトにアクセスします。
コントラクトアドレス |
---|
0x556Ff63Ad1fFEcCdc981EE55c51008097648D59b |
pragma solidity ^0.4.18;
contract SimpleStorage {
uint storedData;
uint[] uintArray;
uint index=0;
event Set(address from,uint stored);
function set(uint x) public{
storedData = x;
uintArray.push(x);
index=index+1;
Set(msg.sender,x);
}
function get() public constant returns (uint retVal) {
return storedData;
}
function get_i() public constant returns (uint i) {
return index;
}
function get_history(uint i) public constant returns (uint retbal,uint result) {
return (i,uintArray[i]);
}
}
今回コントラクト上にはget_history関数を作りました。
これは引数を入力すると、配列uintArrayに格納した値が返されるようにしています。
uintArrayはset関数を実行するとことで、storeDataを配列に溜め込むようにしています。
get()とget_i()はそれぞれ直近のstoreDataと、indexの値を返します。
Step1と同じ要領で、json形式のデータを投げればいいことがわかります。
例えば、set(495)を実行する場合は次のようになります。toはコントラクトアドレス、fromはESP8266が接続しているノードが保有するアカウントアドレスになります。
String eth_sendTransaction(String from,String to,String data){
String payload =String("{\"method\":\"eth_sendTransaction\",\"params\":[{\"from\":\""+from+"\",\"to\":\""+to+"\",\"data\":\""+data+"\"}],\"id\":0}");
return payload;
}
String to="0x556Ff63Ad1fFEcCdc981EE55c51008097648D59b";
String from="0xe60210acda3b8897a689062bdee0cf25e112dc2f";
String data="0x60fe47b100000000000000000000000000000000000000000000000000000000000001ef";
eth_sendTransaction(from,to,data);
ここでdataは次のように指定しています。
String data="0x60fe47b100000000000000000000000000000000000000000000000000000000000001ef";
これはEthereumのコントラクトABIのドキュメントに詳細が書いてありますが、ざっくりいうと、コントラクトの呼び出しは、関数名(メソッドID)+引数の組み合わせで呼び出すことになるためです。
それぞれ確認していきます。
###■メソッドID
関数呼び出しにかかる先頭4バイトが、呼び出そうとする関数のメソッドIDになります。
メソッドIDはハッシュ関数であるKeccak(SHA-3アルゴリズム)のハッシュ値先頭4バイトになります。
gethのコンソール上ではハッシュ関数が実行できますのでメソッドIDが確認できます。
(事前にRAS2のコンソールなどを開いてgethを起動して確認しておきます。)
例えばコントラクトに記載したset(uint256)を呼び出す場合は次のようにします。
(sha3ハッシュ値の先頭4バイトだけ抜き出します。)
> web3.sha3("set(uint256))").substring(0,10)
"0x60fe47b1"
###■引数
16進数にエンコードされた値です。
コントラクト上の関数に記載された引数の型により、何バイトの文字列として指定するかが変わってきます。
ドキュメント上ではuint256は32バイト(64桁)の16進文字列として指定する仕様となっています。
例えば、10進数の"495"を入力する場合、16進数では"1ef"です。
32バイトでuint256の引数を表現しますので、残りはの部分はすべて0埋めすることになります。
00000000000000000000000000000000000000000000000000000000000001ef
最後にメソッドIDと引数のそれぞれの文字列を連結させます。
メソッドID(4バイト)+引数(32バイト)で合計36byteの72桁文字列になります。
(参考:uint256の引数が2つになると、(4バイト+32バイト+32バイト=68バイトの136桁文字列になります)
0x60fe47b100000000000000000000000000000000000000000000000000000000000001ef
上記のように、メソッドIDを取得するのはsha3のハッシュ関数を通さないといけないので、gethでsha3(関数名)を実行した時に確認できる文字列を事前に控えておきます。
関数名 | 引数 | sha3を通した結果(先頭4バイト) |
---|---|---|
set | uint256 | 0x60fe47b1 |
get_history | uint256 | 0x9b25708c |
なおgeth_history()はstorDataに格納した配列を参照するだけなのでeth_callメソッドを使います。
先ほどのコントラクト実行と同じ要領でget_history(13)を String dataで表現してます。
String eth_call(String to,String data){
String payload =String("{\"method\":\"eth_call\",\"params\":[{\"to\":\""+to+"\",\"data\":\""+data+"\"},\"latest\"],\"id\":0}");
return payload;
}
String data="0x9b25708c000000000000000000000000000000000000000000000000000000000000000d";
String to="0x556Ff63Ad1fFEcCdc981EE55c51008097648D59b";
String eth_call(to,data)
これを実行するとこんな感じになります。
なんだか、とても長ったらしい数字が出てきました。
eth_callのresultだけで128桁あります。これはeth_callによって呼び出したget_history関数の戻り値がuint256 i,uint256 uintArray[i]の2つであるため、32byte(64桁)×2となり、128桁分が16進数文字列として返却されることになるためです。
これを解析すると、次のような値を返していることがわかります。
index=(d)HEX ⇛13
uintArray[13]=(114)HEX⇛276
最終的に、eth_callメソッド、eth_transactionを実行するためには次のように記載することとなります。
(ESP8266の起動初回のみを実行する場合です)
eth_transanctionを実行する場合には、アカウントをアンロックする必要もあります。
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
String eth_blockNumber(){
String payload ="{\"method\":\"eth_blockNumber\",\"params\":{},\"id\":0}";
return payload;
}
String eth_call(String to,String data){
String payload =String("{\"method\":\"eth_call\",\"params\":[{\"to\":\""+to+"\",\"data\":\""+data+"\"},\"latest\"],\"id\":0}");
return payload;
}
String eth_sendTransaction(String from,String to,String data){
String payload =String("{\"method\":\"eth_sendTransaction\",\"params\":[{\"from\":\""+from+"\",\"to\":\""+to+"\",\"data\":\""+data+"\"}],\"id\":0}");
return payload;
}
String personal_unlockAccount(String address,String passwd){
String payload ="{\"method\":\"personal_unlockAccount\",\"params\":[\""+address+"\",\""+passwd+"\"],\"id\":0}";
return payload;
}
String personal_lockAccount(String address){
String payload ="{\"method\":\"personal_lockAccount\",\"params\":[\""+address+"\"],\"id\":0}";
return payload;
}
void http_post(String payload){
HTTPClient http;
http.begin("http://192.168.100.100:8545/");
http.addHeader("Content-Type", "application/json");
Serial.println(payload);
int httpCode = http.POST(payload);
Serial.printf("[HTTP] POST... code: %d\n", httpCode);
Serial.println(http.getString());
}
void setup()
{
Serial.begin(115200);
Serial.println();
WiFi.begin("MYSSID", "PASSWORD");
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
http_post(eth_blockNumber());
//eth_call
String data="0x9b25708c000000000000000000000000000000000000000000000000000000000000000d";
String to="0x556Ff63Ad1fFEcCdc981EE55c51008097648D59b";
http_post(eth_call(to,data));
//personal_unlockAccount
delay(5000);
String address="0xe60210acda3b8897a689062bdee0cf25e112dc2f";
String passwd="passwd";
http_post(personal_unlockAccount(address,passwd));
delay(15000);
//eth_sendTransaction
String from="0xe60210acda3b8897a689062bdee0cf25e112dc2f";
data="0x60fe47b100000000000000000000000000000000000000000000000000000000000001ef";
http_post(eth_sendTransaction(from,to,data));
delay(3000);
//peronal_lockAccount
http_post(personal_lockAccount(address));
delay(3000);
}
void loop() {
}
##最後に(利用例)
ESP8266はGPIOがあるので、I2Cに対応したLCDや温度センサも接続することがきます。
上の写真のようにI2C対応の温度センサに接続してプライベートネットワーク上のコントラクトを実行し、温度データをブロックチェーンに登録したりできます。(ここでは前述のコントラクトのuintArrayに登録しつづけています)
安価なモジュールでもEthereumのノードにアクセスできて、簡単にトランザクションを飛ばすことができるので、ブロックチェーン版のAmazonDashのようなものが国内で少しずつでてくるかもしれません。
用途は色々とあるかと思いますが、ESP8266は色々と夢が広がるデバイスだと思いました。
参考にしたもの: