DMM.comの、一番深くておもしろいトコロ。

Go言語でEthereumへ接続し、DAppsを実行する

Go言語でEthereumへ接続し、DAppsを実行する

  • このエントリーをはてなブックマークに追加

はじめに

はじめまして。DMM.comラボ CTO室のKIMと申します。 最先端技術の調査や検証・研究開発をしております。

今回は、Ethereum DApps(Decentralized Applications: 分散型アプリケーション)開発におけるツールとその使い方についてご紹介したいと思います。記事で紹介するソースコードはここにありますので、是非参考にしてみてください。

GoでDApps開発をしたい

最近EthereumのクライアントライブラリであるWeb3.jsを各言語にコンバートしたライブラリや便利な開発ツールがいろいろ出てきてますね!

例えば

言語 ライブラリ(ツール)
JavaScript Web3.js
Python Web3.py
Java web3j(非公式)
Ruby ethereum.rb(非公式)

上記の表のように他の言語には代表的な便利なツールやライブラリがありますが、 Goにはこれといった開発ツールやライブラリは今までありませんでした(ライブラリを使わなくてもGoでDAppsを開発することはできますが、その手順ややり方がややこしく、そのまま使うには少しハードルが高いものになっています)。

そこで、Go実装の開発ツールであるPerigordを使って、Ethereumに接続し、DAppsを実行するところまで試してみました。

Perigordとは

Perigordは、Goで簡単かつ効率良くDAppsが開発できるツール(フレームワーク)です。

上述したように、 GoでDAppsを開発することはできますが、その手順ややり方がややこしく、そのまま使うには少しハードルが高いものになっています。そういった部分を使いやすく、シンプルにしたのがこのPerigordです。 Truffleに似ていて、PolySwarmというプロダクトの基盤技術として使われています。

github.com

使い方

Perigordは、かなりシンプルで、コマンドを叩くだけでプロジェクト作成ができたり、コントラクトが追加できたりします(もちろんビルドも、テストもです)。 Truffleを経験したことがある人であればTruffleに似ていることがすぐわかると思います(実際に、Perigordの作者はTruffleからいろいろと影響を受けたと語っています)。

Perigordでよく使うコマンド一覧

コマンド 説明
perigord init <プロジェクト名> 新規プロジェクト初期化
perigord build ビルド
perigord add contract <コントラクト名> 新規Contract作成
perigord add migration <コントラクト名> migration追加
perigord add test <コントラクト名> test追加
perigord test test実行

簡単なDAppsを作ってみる

ここで簡単なDAppsの作り方をご紹介したいと思います。 まずインストール方法から見てみましょう。

事前準備

Perigordを利用するには、下記のものが必須となります。

  • Go
  • Geth
  • Solc
  • abigen

今回は、Macで検証を行なったため、Macベースでのインストール方法をご紹介します。

前提条件

  • Homebrewインストール済
  • Goインストール済(GOPATHの設定も含む)
  • ここではGethやSolidity言語に関する詳細なお話はしません。ご了承ください

Geth、Solcのインストール

Geth、Solcのインストールは、Homebrewで行います。

$ brew tap ethereum/ethereum
$ brew install ethereum solidity

abigenインストール

abigenは、コントラクトを簡単かつ安全にBindingできるようにABIベースのGo Codeを生成してくれるツールです。

go getコマンドでインストールします。

$ go get github.com/ethereum/go-ethereum
$ pushd $GOPATH/src/github.com/ethereum/go-ethereum
$ go install ./cmd/abigen
$ popd

Perigordインストール

今回の主人公です。

go getコマンドでインストールします。

$ go get -u github.com/polyswarm/perigord/... && \
    go get -u github.com/jteeuwen/go-bindata/...

確認

perigordコマンドを叩いて、下記のような画面が表示されたら正常にインストール完了です。

$ perigord
A golang development environment for Ethereum

Usage:
  perigord [command]

Available Commands:
  add         Add a new contract or test to the project
  build       (alias for compile)
  compile     Compile contract source files
  deploy      (alias for migrate)
  generate    (alias for compile)
  help        Help about any command
  init        Initialize new Ethereum project with example contracts and tests
  migrate     Run migrations to deploy contracts
  test        Run go and solidity tests

Flags:
  -h, --help   help for perigord

Use "perigord [command] --help" for more information about a command.

新しいプロジェクト作成

perigordコマンドを使って新しいDAppプロジェクトを作成します。

$ perigord init perigordTestDapp
$ pushd $GOPATH/src/perigordTestDapp

下記のようなプロジェクトのディレクトリ構成になります。

$ tree
.
├── contracts
│   └── Foo.sol
├── generate.go
├── main.go
├── migrations
│   └── 1_Migrations.go
├── perigord.yaml
├── stub
│   ├── README.md
│   └── main.go
├── stub_test.go
└── tests
    └── Foo.go

コントラクト作成

Greeterという名前の新しいコントラクトを生成します。

$ perigord add contract Greeter

生成したコントラクトの中身を下記のように変更します。

contracts/Greeter.sol

pragma solidity ^0.4.4;

contract Greeter {
    string public greeting;

    event Result(address from, string stored);

    function Greeter() {
        greeting = "Hello";
    }

    function setGreeting(string _greeting) public {
        greeting = _greeting;
        Result(msg.sender, greeting);
    }

    function greet() constant returns (string) {
        return greeting;
    }
}

ビルド

作成したコントラクトをビルドします。

$ perigord build

ビルドが完了すると、bindingsbuildディレクトリが生成されます。

また、生成された各ディレクトリの下にデプロイに必要なファイルが生成されていることがわかります。

$ tree
.
├── bindings
│   ├── Foo.go
│   └── Greeter.go
├── build
│   ├── Foo.abi
│   ├── Foo.bin
│   ├── Foo.link
│   ├── Greeter.abi
│   ├── Greeter.bin
│   └── Greeter.link
. . .

マイグレーション

コントラクトをデプロイするには、マイグレーションを追加する必要があります。

$ perigord add migration Greeter

migrationsディレクトリに2_Greeter.goというファイルが生成されます。

$ tree
. . .
├── migrations
│   └── 1_Migrations.go
│   └── 2_Greeter.go
. . .

GoでDAppsを操作する

GoでDAppsを操作するために、Goでコードを書きます。

今回は3つのファイルに分けて、DAppsの操作に必要な機能やデータインタフェースを作ります。

main.go

今回のサンプルのメイン処理を定義するファイルです。

main関数の中の流れは、下記の順になります。

     初期化
      ↓
     デプロイ
      ↓
   コントラクト接続
      ↓
イベント監視用チャンネル設定
      ↓
   データ取得と更新
      ↓
 イベント取得とデータ取得
package main

import (
    "context"
    "fmt"
    "perigordTestDapp/bindings"
    _ "perigordTestDapp/migrations"
    "log"

    "github.com/polyswarm/perigord/contract"
    "github.com/polyswarm/perigord/migration"
    "github.com/polyswarm/perigord/network"
)

var greeter *Greeter

func main() {
    // 初期化
    network.InitNetworks()

    // perigord.yamlから情報を取得し、接続を行う
    nw, err := network.Dial("dev")
    if err != nil {
        log.Fatalln("Could not connect to dev network: ", err)
    }

    // マイグレーション実行(デプロイ)
    if err := migration.RunMigrations(context.Background(), nw, false); err != nil {
        log.Fatalln("Error running migrations: ", err)
    }

    // コントラクト接続
    session, ok := contract.Session("Greeter").(*bindings.GreeterSession)
    if !ok {
        log.Fatalln("Error retrieving session")
    }

    greeter = NewGreeter(session, nw.Client())
    // イベント監視用チャンネル
    eventChan := make(chan *Event)
    if err := greeter.WatchForEvents(eventChan); err != nil {
        log.Println("error listening for incoming events:", err)
        return
    }

    // データ取得
    fmt.Printf("更新前: %s\n", greeter.Greet())

    // データ更新
    greeter.SetGreeting("Hello, World!:D")

    //  イベント監視
    for {
        event := <-eventChan
        fmt.Println("Event結果\n")
        fmt.Printf("from: %v\n", event.Body.(*NewEvent).From.Hex())
        fmt.Printf("stored: %v\n", event.Body.(*NewEvent).Stored)
        // 先ほど更新した内容("Hello, World!:D")に更新されているか確認
        fmt.Printf("更新後: %s\n", greeter.Greet())
    }
}

greeter.go

Greeterコントラクトに定義されている機能を実行します。GetやSet、Event監視機能があります。

package main

import (
    "context"
    "encoding/json"
    "perigordTestDapp/bindings"
    "log"
    "strings"

    ethereum "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/polyswarm/perigord"
    "github.com/polyswarm/perigord/contract"
)

type Greeter struct {
    session *bindings.GreeterSession
    client  *ethclient.Client
}

func NewGreeter(session *bindings.GreeterSession, client *ethclient.Client) *Greeter {
    return &Greeter{
        session: session,
        client:  client,
    }
}

func (g *Greeter) Greet() string {
    greet, _ := g.session.Greet()
    return greet
}

func (g *Greeter) SetGreeting(greeting string) error {
    if result, _ := g.session.SetGreeting(greeting); result == nil {
        log.Fatalln("failed set greeting.")
    }
    return nil
}

// イベント監視
func (g *Greeter) WatchForEvents(eventChan chan *Event) error {
    topics := map[string]common.Hash{
        "Result": perigord.EventSignatureToTopicHash("Result(address,string)"),
    }

    q := ethereum.FilterQuery{
        Addresses: []common.Address{contract.AddressOf("Greeter")},
        Topics: [][]common.Hash{{
            topics["Result"],
        }},
    }

    dec := json.NewDecoder(strings.NewReader(bindings.GreeterABI))
    var abi abi.ABI
    if err := dec.Decode(&abi); err != nil {
        return err
    }

    logChan := make(chan types.Log)
    sub, err := greeter.client.SubscribeFilterLogs(context.Background(), q, logChan)
    if err != nil {
        return err
    }
    go func() {
        log.Println("Starting event monitor")
        for {
            select {
            case logMsg := <-logChan:
                if len(logMsg.Topics) != 1 {
                    log.Println("incorrect number of topics")
                    break
                }

                var nbe NewEventLog
                if err := abi.Unpack(&nbe, "Result", logMsg.Data); err != nil {
                    log.Println("error unpacking log: ", err)
                    break
                }

                event := &Event{
                    Type: "Result",
                    Body: NewEventFromLog(nbe),
                }
                eventChan <- event
                break
            case err := <-sub.Err():
                log.Println(err)
                break
            }
        }
    }()

    return nil
}

event.go

Event監視の時に使うデータインタフェースを定義します。

package main

import "github.com/ethereum/go-ethereum/common"

type Event struct {
    Type string
    Body interface{}
}

type NewEventLog struct {
    From   common.Address
    Stored string
}

type NewEvent struct {
    From   common.Address
    Stored string
}

func NewEventFromLog(nbe NewEventLog) *NewEvent {
    return &NewEvent{
        From:   nbe.From,
        Stored: nbe.Stored,
    }
}

実行

まず、testnetを実行します。

インストールしたperigordディレクトリの中に、testnetの実行スクリプトファイルがあるので、それを使ってtestnetを実行します(別のターミナルで実行することをお勧めします)。

$ $GOPATH/src/github.com/polyswarm/perigord/scripts/launch_geth_testnet.sh

次に、ビルドの前にファイルの内容を少し修正します。

Perigord buildコマンドで生成されたものをそのままビルド・実行すると、実行後にアカウントのパスワードを聞かれるので、ビルド前にmigrationsディレクトリにあるファイルを修正します。

migrations/1_Migrations.go, migrations/2_Greeter.go

. . .
func (d *GreeterDeployer) Deploy(ctx context.Context, network *network.Network) (common.Address, *types.Transaction, interface{}, error) {
    account := network.Accounts()[0]
    // 下記の行をコメントアウトもしくは削除
    network.UnlockWithPrompt(account)
    // 下記の行を追加
    network.Unlock(account, "blah")

    . . .
}

func (d *GreeterDeployer) Bind(ctx context.Context, network *network.Network, address common.Address) (interface{}, error) {
    account := network.Accounts()[0]
    // 下記の行をコメントアウトもしくは削除
    network.UnlockWithPrompt(account)
    // 下記の行を追加
    network.Unlock(account, "blah")

    . . .
}
. . .

最後に、go buildコマンドでGoファイルをビルドし、実行します。

$ go build main.go greeter.go event.go
$ ./main

実行すると、下記のような結果が画面に表示されます。

f:id:jkcomment:20180411105904p:plain
実行結果

終わりに

まだプロジェクト自体の歴史も浅いですし、Truffleなどの他のツールに比べるとまだまだって感じがしますが、 GoでDAppsの開発ができるのはやはり大きいかなと思います。今後が楽しみですね!

参考

Introducing Perigord: Golang Tools for Ethereum DApp Development

GitHub - polyswarm/perigord: Perigord: Golang Tools for Ethereum Development

Native DApps: Go bindings to Ethereum contracts · ethereum/go-ethereum Wiki · GitHub

go ethereum - How to decode Log.Data in Go - Ethereum Stack Exchange

採用情報

現在、CTO室では、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい! dmm-corp.com