blockchain上で自動支払を行うスマートコンセントを作ってみる

# 自動支払をするスマートコンセント

Ethereumのスマートコントラクトを書いてみたくて、スマートコンセントを練習として作ってみました。

物理的にはひとまずこのような形状をしています。筐体を３Dプリントしようとしましたが、一旦力尽きました。。

<img width="870" alt="Screen Shot 2017-12-28 at 18.17.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/3ed06141-2795-a06b-ca08-78f08676661f.jpeg">

概要は下記のような感じです。

<img width="870" alt="Screen Shot 2017-12-28 at 18.17.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/e1200f8f-437c-0afc-2be7-eb34da079494.png">

このコンセプトは「C２Cで一般ユーザが別のユーザに電気を販売することができる」モデルですが、自分でサーバは立てておらず、2つのデバイスがBlockchainを経由してtrasnsactionを行います。uberやairbnbとは違い中央集権的なサーバが不要となっているため、運営が不在の状況でもサービスを提供することができるかもしれません。[^1]

使っているフレームワークなどは以下のとおりです。

<img width="864" alt="Screen Shot 2017-12-28 at 18.18.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/54ab63da-af56-339d-9f83-7e47cca7598a.png">

それぞれのモジュールの間の通信はこのようになっています。

<img width="870" alt="Screen Shot 2017-12-28 at 18.18.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/a532c869-e6d7-c776-5c58-1ef497e13d20.png">

似たようなプロジェクトが幾つか発表されています。

- [Nayuta](https://btcnews.jp/nayuta-released-iot-blockchain-powered-power-socket/)
    - コンセント側は同じようなコンセプトで、支払いをスマホで行っているところは少し違うようです。今回はスマホレスにしてみたかったため、プラグにコンピュータを組み込みました
    - プロダクトデザインカッコいいな。
- [BlockCharge](https://www.youtube.com/watch?v=0A0LqJ9oYNg)
    - 電気スタンドと電気自動車の間で自動で支払いを行うかたちです。
    - ドローンともつながってワイヤレス給電する未来いいな
- [Toyota Open Road Project](http://openroad-project.com/prototyping/smile-lock)
    - blockchainを使っているかはわからないですが、似たコンセプトです。
    - プロジェクト更新止まっちゃってるのかな、、

githubこちらですが、まだバグバグなので頑張ります、、

https://github.com/kopanitsa/blockchain-smart-outlet

# スマートコントラクトを作成する

Ethereum上で動作するスマートコントラクトをsolidityで書いています。
シーケンス図の3で、コンセントからhashが送られてくると、コントラクトはhashをmapに保存します。

```js
mapping(bytes32 => address) reserved;

function sendOutletHash(bytes32 _hash) public {
    reserved[_hash] = msg.sender;
}
```

その後、シーケンス図8で、keyをコントラクトに送信します。ここで、keyとhashの照合が行われ、支払い(`coin`の移動)が実施されます。正常に取引が完了すると、`event`がコンセントに送信されます(シーケンス図の10)。

```js
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です。

**プラグ側**

<img width="720" alt="Screen Shot 2017-12-28 at 19.42.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/0c984636-99f3-f2fb-c1e0-e6e7eeefe7e7.png">

**コンセント側**

<img width="740" alt="Screen Shot 2017-12-28 at 19.42.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/42108/73fd6164-a9aa-c44f-64de-6b18a4876b5a.png">

なお、コンセント側を家の電源(ブレーカーから壁の中を伸びてるやつ)に直接(コンセント等を経由せず)繋げる場合、電気工事士の資格が必要なので注意しましょう。

# プラグ側のコードを作成する

プラグの情報処理は、コンセントに刺されて起動するところから始まります[^2]

全体の流れ、こんな感じです。

```plug/wifi.js
    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](https://www.npmjs.com/package/wireless-tools)というモジュールを使って以下のように実現しました。`wpa_supplicant.enable`でWi-Fiの接続先を変更できます。

```plug/wifi.js
    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`を取得します。

```plug/wifi.js
    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`をコントラクトに送信します。

```plug/coin.js
// すでに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は２つのwifiモジュールを持つ構成としました[^3]。一つはRaspberry Pi 3に組みおまれているもので、上記メインのアプリ用にstation modeで動作します。もう一つはUSB wifiモジュールで上記HTTPサーバにアクセスするためのAPとして動作します。

まずはWebサーバ側。非常にシンプルです。`key`はメインアプリで生成されたあと、一時的に`/home/pi/key.txt`に記録されるとしています。

```outletserver/app.js
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)。

```outlet/gpio.js
    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でハッシュ化したものです。

```outlet/outlet.js
    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};
    }
```

```outlet/coin.js
    // 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し、コンセントの電源を切らないようにすれば完成です。

```outlet/outlet.js
    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など

- スマートコントラクト
    - [Learning Solidity](https://karl.tech/learning-solidity-part-1-deploy-a-contract/)
    - [Etherum geth・Solidity・node.jsを利用したDapps開発初歩](https://qiita.com/tomohata/items/25b37be074d9c0fde37d)
- Geth, Rinkebyテストネット
    - [How to get on Rinkeby Testnet in less than 10 minutes](https://gist.github.com/cryptogoth/10a98e8078cfd69f7ca892ddbdcf26bc)
    - [Connect Geth](https://www.rinkeby.io/#geth)
    - Raspberry PiでEthereumクライアント（geth)をインストールする。(https://qiita.com/umidachi/items/1be8262e41cb32cab369)
- hostapdの設定
    - https://github.com/bakerface/wireless-tools/issues/15

[^1]: じゃあだれが責任をもって運用をおこなうのかとか、もうからないのに誰がつくるのかなどは別のお話です
[^2]: 今回は、プラグが刺されてスイッチが押された際、短時間のみ電源供給をするようにしました
[^3]: 今後wifiモジュール一つで動作するように修正したいと考えています