自動支払をするスマートコンセント
Ethereumのスマートコントラクトを書いてみたくて、スマートコンセントを練習として作ってみました。
物理的にはひとまずこのような形状をしています。筐体を3Dプリントしようとしましたが、一旦力尽きました。。
概要は下記のような感じです。
このコンセプトは「C2Cで一般ユーザが別のユーザに電気を販売することができる」モデルですが、自分でサーバは立てておらず、2つのデバイスがBlockchainを経由してtrasnsactionを行います。uberやairbnbとは違い中央集権的なサーバが不要となっているため、運営が不在の状況でもサービスを提供することができるかもしれません。1
使っているフレームワークなどは以下のとおりです。
それぞれのモジュールの間の通信はこのようになっています。
似たようなプロジェクトが幾つか発表されています。
-
Nayuta
- コンセント側は同じようなコンセプトで、支払いをスマホで行っているところは少し違うようです。今回はスマホレスにしてみたかったため、プラグにコンピュータを組み込みました
- プロダクトデザインカッコいいな。
-
BlockCharge
- 電気スタンドと電気自動車の間で自動で支払いを行うかたちです。
- ドローンともつながってワイヤレス給電する未来いいな
-
Toyota Open Road Project
- blockchainを使っているかはわからないですが、似たコンセプトです。
- プロジェクト更新止まっちゃってるのかな、、
githubこちらですが、まだバグバグなので頑張ります、、
スマートコントラクトを作成する
Ethereum上で動作するスマートコントラクトをsolidityで書いています。
シーケンス図の3で、コンセントからhashが送られてくると、コントラクトはhashをmapに保存します。
mapping(bytes32 => address) reserved;
function sendOutletHash(bytes32 _hash) public {
reserved[_hash] = msg.sender;
}
その後、シーケンス図8で、keyをコントラクトに送信します。ここで、keyとhashの照合が行われ、支払い(coin
の移動)が実施されます。正常に取引が完了すると、event
がコンセントに送信されます(シーケンス図の10)。
mapping(address => uint256) public coinBalance; // remaining coin of each devices
event log(string);
event startSupply(address);
function payFromPlug(string _key, uint256 _coin) public {
bytes32 encrypted = keccak256(_key);
address targetOutlet = reserved[encrypted];
if (targetOutlet == 0) {
log("ERROR: Cannot find ready outlet");
return;
}
if ((int256)(coinBalance[msg.sender] - _coin) < 0) {
log("ERROR: You dont have enough coin to supply!");
return;
}
coinBalance[msg.sender] -= _coin;
coinBalance[targetOutlet] += _coin;
remainingTime[encrypted] = now + 60 * 1 minutes;
startSupply(targetOutlet);
}
ハードウェアを作成する
ハードウェアはざっくり以下のような感じです。赤線が電源、青線がGPIOです。
プラグ側
コンセント側
なお、コンセント側を家の電源(ブレーカーから壁の中を伸びてるやつ)に直接(コンセント等を経由せず)繋げる場合、電気工事士の資格が必要なので注意しましょう。
プラグ側のコードを作成する
プラグの情報処理は、コンセントに刺されて起動するところから始まります2
全体の流れ、こんな感じです。
setup() {
this.disconnect().then(()=> {
return this.connectOutlet();
}).then(()=> {
return this.getKeyFromOutlet();
}).then(()=> {
return this.disconnect();
}).then(()=> {
return this.connectMain();
}).then(()=> {
return this.sendEvent();
}).catch((err) => {
log.warn(err);
});
}
シーケンス図6で、wifiの接続先を通常のアクセスポイント
からコンセントのローカルAP
に変更するのですが、wireless-toolsというモジュールを使って以下のように実現しました。wpa_supplicant.enable
でWi-Fiの接続先を変更できます。
connectOutlet() {
const options = {
interface: 'wlan0',
ssid: 'SmartOutlet',
driver: 'nl80211'
};
return new Promise((resolve, reject) => {
wpa_supplicant.enable(options, (err) => {
if (err) {
reject("connectOutlet:" + err);
}
resolve();
});
});
}
コンセントのローカルAP
に接続できたら、コンセントに立っているhttpサーバからkey
を取得します。
getKeyFromOutlet() {
let _this = this;
return new Promise((resolve, reject) => {
http.get(outlet_server_url, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', (res) => {
_this.key = body;
resolve();
});
}).on('error', (e) => {
reject("http error:" + e);
});
});
}
最後に、接続先を通常のアクセスポイントに戻し、key
をコントラクトに送信します。
// すでにblockchain上にスマートコントラクトをデプロイしており、アドレスを知っているとします
const address_contract = "0x71A2897b2B222225855240c10114FdE25865b3fB";
const Web3 = require('web3');
const net = require('net')
// 事前に、gethはローカル(raspberry pi上)で動作し、IPCで接続できるようにしておきます。
let web3 = new Web3('/home/pi/.ethereum/rinkeby/geth.ipc', net);
let plugContract = new web3.eth.Contract(abi, address_contract);
...
execPayFromPlug(key, coin) {
plugContract.methods.payFromPlug(key, coin).send(
{from: this.address, gasPrice: 2000000000, gas: 3000000},
(error, result)=> {
log.debug("payFromPlug result:" + result);
}).on('transactionHash', (hash) => {
log.debug("transactionHash: " + hash);
})
}
あとは、blockchainのtransactionが完了し、コンセント側で承認完了されるのを待つだけです。
コンセント側のコードを作成する
コンセント側は2つのアプリが動いています。1つはコンセントの電気の流れを制御したり、スマートコントラクトとやりとりするメインのアプリ。もう一つは小さなHTTPサーバで、シーケンス図7でプラグにkey
を渡すのに使用されます。
今回、コンセント側のRaspberry Piは2つのwifiモジュールを持つ構成としました3。一つはRaspberry Pi 3に組みおまれているもので、上記メインのアプリ用にstation modeで動作します。もう一つはUSB wifiモジュールで上記HTTPサーバにアクセスするためのAPとして動作します。
まずはWebサーバ側。非常にシンプルです。key
はメインアプリで生成されたあと、一時的に/home/pi/key.txt
に記録されるとしています。
const http = require('http');
const fs = require('fs');
const ip = '192.168.1.1';
const key = '/home/pi/key.txt';
http.createServer(function (req, res) {
const key = fs.readFileSync(key, 'utf8');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(key);
}).listen(8000, ip);
次にメインのアプリ側です。
メインのアプリはプラグが接続され、スイッチが押されたことをGPIOで検知するところから始まります(シーケンス図1)。
checkSwitch() {
let value = this.readGpio(SW_IN, 'utf8');
if (parseInt(value) == 1) {
this.writeGpio(SSR, 1); // コンセントから電気が流れるようにする
this.emit('switch', true);
this.initialPowerTimer = setTimeout(() => {
this.writeGpio(SSR, 0); // 一定時間後(処理が行われない場合)コンセントからの電気をOFFにする
this.emit('switch', false);
}, 60 * 1000);
}
}
続いて、ワンタイムkeyとhashを生成します。hash
はランダムな16文字として生成したkey
をkeccak256でハッシュ化したものです。
this.gpio.on('switch', (enabled) => {
if (enabled) {
const hashpair = _this.generateKeyAndHash(); // keyとhashを生成する
_this.coin.sendOutletHash('0x' + hashpair.datahash); // hashをblockchainに送付
fs.writeFile(key, hashpair.datakey); // keyをサーバに渡すため、ファイルとして保存
}
});
....
generateKeyAndHash() {
const l = 16;
const c = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const cl = c.length;
let key = "";
for(let i=0; i<l; i++){
key += c[Math.floor(Math.random()*cl)];
}
let hash = createKeccakHash('keccak256').update(key).digest('hex');
return {datakey: key, datahash: hash};
}
// hashを送付
sendOutletHash(hash) {
web3.eth.personal.unlockAccount(this.address, this.password, 60)
.then((response) => {
this.execSendOutletHash(hash)
}).catch((error) => {
console.log(error);
});
}
execSendOutletHash(hash) {
plugContract.methods.sendOutletHash(hash).send(
{from: this.address,
gasPrice: 20000000000,
gas: 3000000},
(error, result)=> {
}).on('transactionHash', (hash) => {
log.debug("transactionHash: " + hash);
});
}
その後、コントラクト上でkey
とhash
の照合が行われ、startSupply(address)
というeventがbroadcastされるので、それをsubscribeし、コンセントの電源を切らないようにすれば完成です。
startPaymentWatching() {
let subscription = web3.eth.subscribe('logs', {
address: address_contract,
topics: [web3.utils.sha3('startSupply(address)')]
}, (error, result) => {
if (error)
log.error(error);
this.emit('contract', true);
});
}
今後
まだまだバグや動きが不審な点があるので修正しつつ、以下のようなあたりを改善していきたいと思います。
- コントラクトのテストを書く
- 本体のテストも書く
- 取引を見える化する
- ここで結局Webサーバが必要になるのか、、
- ハードウェアをもう少しちゃんとつくる
- Raspberry Pi(Linux)じゃなくてRTOSとかで動けば、プラグデバイスの起動時間も短く、価格も安くなりそうなのですが、geth的なものをRTOSまたはOSなしで動かすことってできるのかな、、
- 取引用コインの入出金の方法を検討する
- テストネットではなく本線ネットにつなげてみる??
参考URLなど
- スマートコントラクト
- Geth, Rinkebyテストネット
- How to get on Rinkeby Testnet in less than 10 minutes
- Connect Geth
- Raspberry PiでEthereumクライアント(geth)をインストールする。(https://qiita.com/umidachi/items/1be8262e41cb32cab369)
- hostapdの設定