はじめに
この記事は以下記事を参考にKotlinでシンプルなブロックチェーンを実装したことをまとめています。
- ブロックチェーンを作ることで学ぶ 〜ブロックチェーンがどのように動いているのか学ぶ最速の方法は作ってみることだ〜
- Learn Blockchains by Building One -The fastest way to learn how Blockchains work is to build one-
元記事はPythonを使って実装されていますが、ちょうどKotlin(サーバーサイド)で何か作ってみたいと思っていたところだったためブロックチェーンへの理解を深めることもでき一石二鳥だということでKotlinでの実装を進めてきました。
Kotlin詳しい方、ブロックチェーン詳しい方、ぜひフィードバックいただけると助かります。
2018/5/2 追記
せっかくだったので元記事をかいていたDaniel van FlymenさんにPRをしておきました。
JavascriptとかrubyはPRあったんですがkotlinはさすがにまだなかったですw
おそらくKotlin詳しい方ではないと思うのでお話ししながらになりそうですがレビュー頂ける方ぜひ。
環境
- Kotlin version 1.2.21-release-88 (JRE 1.8.0_73-b02)
- IntelliJ Community (IDE上でビルドしながら開発進めてました)
- Docker version 18.03.0-ce, build 0520e24
- https://github.com/masayuki5160/kotlin-blockchain
IntelliJ上で開発は進めています。
コンセンサスアルゴリズムの動作テストをするときになってもう一つホストが欲しくなりDockerfileを作っていました。
Docker環境があれば以下手順で環境は作れると思います。
# gitからcloneしてくる
$ git clone git@github.com:masayuki5160/kotlin-blockchain.git
$ cd kotlin-blockchain
# DockerfileからDockerイメージをビルド
$ docker build -t masayuki5160/kotlin-blockchain .
# ビルドしたイメージからコンテナ起動
$ docker run -d -p 4567:4567 -p 8778:8778 masayuki5160/kotlin-blockchain
# 動作テスト(Genesisブロックのみ登録されたチェーンがJSON形式でかえってくる)
$ curl -s http://localhost:4567/chain
Dockerコンテナが作成できていると以下のようにレスポンスがかえってきます。
ジェネシスブロックのみ登録された状態です。
{
"chain": [
{
"index": 1,
"timestamp": 1525151491,
"transactions": [],
"proof": 100,
"previousHash": "1"
}
],
"length": 1
}
ちなみにKotlinをDocker環境で動作させるとき下記記事を参考にしました。
プログラムについて
Pythonで書かれた元記事の方の説明が非常にわかりやすいので詳細は割愛しようと思います。
今回はKotlinで実装したということで使用したライブラリ等だけまとめます。
Spark
トップ絵に記載のあるようにmicro frameworkとなってます。
Kotlin Webアプリケーションでも紹介があり僕はそこで存在を知りました。
例えば今回のアプリケーションのエントリーポイントはSparkだとこんな感じになっています。
import blockchain.*
import spark.Spark.get
import spark.Spark.post
import spark.Spark.delete
import spark.Spark.path
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.util.UUID
fun main(args: Array<String>){
val nodeId = UUID.randomUUID().toString().replace("-", "")
val objectMapper = ObjectMapper().registerKotlinModule()
val jsonTransformer = JsonTransformer(objectMapper)
val blockChain = Blockchain()
val controller = Controller(objectMapper, blockChain, nodeId)
path("/transactions") {
post("/new", controller.addTransaction(), jsonTransformer)
}
path("/mine") {
// 新しいBlockを採掘する
get("", controller.mine(), jsonTransformer)
}
path("/chain") {
// フルのブロックチェーンをリターンする
get("", controller.fullChain(), jsonTransformer)
}
path("/nodes") {
post("/register", controller.registerNode(), jsonTransformer)
get("/resolve", controller.resolveNode(), jsonTransformer)
}
}
path()でグルーピングをしていますがこれはなしでも動作すると思います。
エントリーポイントが増えてくると大変そうですが今回のプログラム程度だと全く問題ない印象です。
get()、post()の引数に渡しているcontroller.・・・は関数型インタフェースRouteのオブジェクトになっており以下のようにCotroller.ktに処理をまとめています。
import blockchain.*
import blockchain.model.*
import com.fasterxml.jackson.databind.ObjectMapper
import spark.Route
import spark.Spark.halt
class Controller(private val objectMapper: ObjectMapper,
private val blockchain: Blockchain,
private val nodeId: String) {
fun fullChain(): Route = Route { req, res ->
Chain(blockchain.chain, blockchain.chain.count())
}
fun mine(): Route = Route { req, res ->
val lastBlock: Block = blockchain.lastBlock()
val lastProof = lastBlock.proof
val proof = blockchain.proofOfWork(lastProof.toString())
// proofを発見した報酬を獲得(senderを0とすることでマイニング実行者の報酬としている)
blockchain.newTransaction(
Transaction("0", nodeId, 1)
)
// チェーンに新しいブロックを追加することで新しいブロック採掘完了
blockchain.addBlock(proof)
"新しいブロックを採掘しました"
}
fun registerNode(): Route = Route { req, res ->
val request: RegisterNodeRequest =
try {
objectMapper.readValue(req.bodyAsBytes(), RegisterNodeRequest::class.java)
} catch (e: Exception) {
throw halt(400)
}
val node = Node(request.url)
blockchain.registerNode(node)
"新しいnodeを登録完了"
}
fun resolveNode(): Route = Route { req, res ->
val replaced = blockchain.resolveConflicts()
val message: String
if (replaced) {
message = "チェーンが置き換えられました"
} else {
message = "チェーンが確認されました"
}
res.status(200)
message
}
fun addTransaction(): Route = Route { req, res ->
val request: NewTransactionRequest =
try {
objectMapper.readValue(req.bodyAsBytes(), NewTransactionRequest::class.java)
} catch (e: Exception) {
throw halt(400)
}
val transaction = Transaction(request.sender,request.recipient,request.amount)
blockchain.newTransaction(transaction)
res.status(201)
"トランザクションはブロックに追加されました"
}
}
ただ、フレームワークとしてあまりルールがないので実装を進めていくにつれファイルの配置などがカオスになっていくので注意がいるな、という感じです。
この辺はもうちょいルールがあると嬉しい気がしたんですがマイクロフレームワークてのはこんな感じなんですかね。
Fuel
FuelはKotlinの公式サイトでも確か紹介されているHTTPクライアントライブラリ?と思います。
GitHubでは以下のように記述されています。
The easiest HTTP networking library for Kotlin/Android
今回はコンセンサスアルゴリズムを実装する際に他のホストとHTTP通信をする必要がありました。
そのためFuelを利用しています。
fun resolveConflicts(): Boolean {
var maxLength = chain.count()
var currentChain = chain
nodes.forEach { nodeUrl, node ->
println("HTTPリクエストstart:" + nodeUrl)
FuelManager.instance.basePath = nodeUrl
// 同期処理
val (request, response, result) = "/chain".httpGet().responseObject(GetChainRequest.Deserializer())
when (result) {
is Result.Success -> {
println("HTTPリクエスト成功")
val chain = result.value.chain
val length = result.value.length
if(maxLength < length) {
val mutableChain = chain!!.toMutableList()
if (validChain(mutableChain)) {
// 検証成功のためチェーンを切り替える
maxLength = length
currentChain = mutableChain
println("現在のチェーンより有効なチェーンを確認")
}
}
}
is Result.Failure -> {
println("ERROR:" + result.error)
}
}
}
// 自らのチェーンより長く、有効なチェーンを見つけたため置き換える
if (maxLength > chain.count()) {
println("現在のチェーンより有効なチェーンに置き換える")
chain = currentChain
return true
}
return false
}
登録されたノードの/chainというエントリーポイントに対してGETリクエストをしています。
もう少しいい書き方ありそうな気もしますがとりあえずこんな感じで実装しました。
テストコード
テストコードについてはJUnitが使えたのでimportして実装しました。
あっさりimportして使えたのは驚きでした、すごい。
@Test fun proofOfWorkで発見したhash値の先頭4文字が0になっている() {
val blockchain = Blockchain()
val lastProof = "10"
val proof = blockchain.proofOfWork(lastProof)
println("proof:" + proof)
val hashVal = blockchain.convertToHash(lastProof + proof)
println("hash:" + hashVal)
assertEquals("0000", hashVal.substring(0, 4))
}
コンセンサスアルゴリズムを試してみる
今回作ってるのはなんちゃってブロックチェーンなのでWebサーバでHTTPリクエストをほげほげしながらブロックチェーンのアルゴリズムを学んでみる、という感じです。
ということで最後に以下のような環境でなんちゃってコンセンサスアルゴリズムで遊んでみます。
- IntelliJでビルドしたSparkアプリケーション(node00)
- DockerコンテナでビルドしたSparkアプリケーション(noe01)
IntelliJでのアプリケーション起動(node00)
IntelliJでのアプリケーション起動方法については割愛します。
(おそらくGradleがよしなにやってくれるので起動できるはず。。)
正常に動作していれば以下コマンドでGenesisブロックの入った情報が確認できるはずです。
4567はSparkのデフォルト起動ポートです。
$ curl -s http://localhost:4567/chain
コンテナ起動(node01)
ポート4567はすでに使用しているためホストのポート4568とコンテナの4567ポートを繋ぎます。
$ git clone git@github.com:masayuki5160/kotlin-blockchain.git
$ cd kotlin-blockchain
# Dockerfileからイメージ作成
$ docker build -t masayuki5160/kotlin-blockchain .
# コンテナ起動(名前をnode01とする)
$ docker run -d -p 4568:4567 -p 8779:8778 --name "node01" masayuki5160/kotlin-blockchain
node01にトランザクション、ブロックを追加
# トランザクションを追加
$ curl -s http://localhost:4568/transactions/new -X POST -d '{"sender":"testSender01","recipient":"testRecipient02","amount":1}'
$ curl -s http://localhost:4568/transactions/new -X POST -d '{"sender":"testSender03","recipient":"testRecipient04","amount":1}'
# マイニングを実行(ブロック追加)
$ curl -s http://localhost:4568/mine
チェーンを確認します。
$ curl -s http://localhost:4568/chain
正常にブロックが追加されていると以下のようなレスポンスが得られます。
トランザクションの3つ目はマイニング成功者への報酬となるトランザクションになっています。
{
"chain": [
{
"index": 1,
"timestamp": 1525147794,
"transactions": [],
"proof": 100,
"previousHash": "1"
},
{
"index": 2,
"timestamp": 1525147821,
"transactions": [
{
"sender": "testSender01",
"recipient": "testRecipient02",
"amount": 1
},
{
"sender": "testSender03",
"recipient": "testRecipient04",
"amount": 1
},
{
"sender": "0",
"recipient": "ef56ba00c8584882ba61a4c306be67ef",
"amount": 1
}
],
"proof": 35293,
"previousHash": "727bf0ec5d936bf4da6388507a1505068e4bae002d727e9156974c684860f7d2"
}
],
"length": 2
}
ノードを追加する
Docker上で起動しているnode01の情報をIntelliJで起動させているnode00に追加します。
といってもPOSTでURL情報をいれてやるだけですw
$ curl -s http://localhost:4567/nodes/register -X POST -d '{"url":"http://localhost:4568"}'
これでコンセンサスアルゴリズムを試す準備ができました。
コンセンサスアルゴリズムの実行
今、node00はGenesisブロックのみが登録された状態です。
一方でnode01はマイニングを実行したため2ブロックある状態になっています。
そのためnode00はnode01からチェーンの情報を取得、検証し、問題なければより長いチェーンを採用します。
# コンセンサスアルゴリズムの実行
$ curl -s http://localhost:4567/nodes/resolve
問題がなかった場合、以下のようにnode00のチェーンを確認するとnode01のチェーンが採用されています。
$ curl -s http://localhost:4567/chain
{
"chain": [
{
"index": 1,
"timestamp": 1525147794,
"transactions": [],
"proof": 100,
"previousHash": "1"
},
{
"index": 2,
"timestamp": 1525147821,
"transactions": [
{
"sender": "testSender01",
"recipient": "testRecipient02",
"amount": 1
},
{
"sender": "testSender03",
"recipient": "testRecipient04",
"amount": 1
},
{
"sender": "0",
"recipient": "ef56ba00c8584882ba61a4c306be67ef",
"amount": 1
}
],
"proof": 35293,
"previousHash": "727bf0ec5d936bf4da6388507a1505068e4bae002d727e9156974c684860f7d2"
}
],
"length": 2
}
まとめ
今回ははじめてのサーバサイドKotlinにトライしてみました。
僕の気づきとしてこんなのがありました。
- IntelliJがうまくサポートしてくれるのでKotlinかいててすごい楽
- Javaの経験多い人は色々はかどるはず
- サーバサイドKotlinの情報もぼちぼち増えてきてる気がする
ブロックチェーンに関しては元記事の方がいうようになんちゃってでいいので実装してみると確かに理解が深まります。
特にブロックチェーンは、技術としても未来像としても残念なものであるより引用しますが体感としてこれはその通りだな〜と実感することができました。
というわけで、ブロックチェーンという「技術」について要約すると次のようになる。
「とても長い、小さなファイルの配列を作ろう。
それぞれのファイルには、ファイルのハッシュ値と、新しいデータと、難しい計算問題の答えを入れるようにし、データを記録してくれる人たちに、決まった時間ごとにいくらかの報酬を渡そう。」
原文は以下。
So in summary, here’s what blockchain-the-technology is: “Let’s create a
very long sequence of small files — each one containing a hash of the
previous file, some new data, and the answer to a difficult math problem
— and divide up some money every hour among anyone willing to certify and
store those files for us on their computers.”
P2Pのところはもちろん実装してないのあれですがチェーンがどうなっているか把握するだけでも今後ブロックチェーンのフレームワークを使う時にも一歩踏み込んで理解ができそうです。
(ちなみに僕自身はブロックチェーン信者なのでブロックチェーンの未来を残念とは思ってないですw)
以上です。
Kotlin詳しい方、ブロックチェーン詳しい方、ぜひフィードバックいただけると助かります。