詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

パッケージ指定で "go run" する

ちょっとしたコマンドラインツールを作ってるときはよく go run main.go するんだけど、作業ディレクトリが main.go から離れてしまうと go run $GOPATH/src/... みたいなことをする羽目になり、ちょっとありがたくない。ファイルが増えてくると go run *.go することになるが、テストコード(*_test.go)が入ってくると go run できないので、そいつを除いてやる必要もあり面倒。

そこで go list を使ってパッケージ名からソースコードを一覧し、それを go run に渡してやる簡単なシェルスクリプトを書いた。

GitHub - motemen/gorun: Run Go programs by their package path

使い方はこんな感じ。

usage: gorun [-l] [-tags tags] packages [arguments...]

gorun github.com/motemen/ghq みたいに、main パッケージへのパスを指定すると go run 的なことをしてくれます。手元のソースコードを動かしたいなら、go run main.go の代わりに gorun . すればよい。

-l フラグをつけると go run する代わりにソースコードをリストアップしてくれるので、entr みたいなのに渡すのに便利。

実際に go run するところは

go run "${flags[@]}" -exec "bash -c 'shift; exec \"\$0\" \"\$@\"'" "${files[@]}" -- "$@"

となっているんだけど、この -exec がミソで、go run するプログラムの引数に *.go なファイルを渡したいようなときに、プログラムのソースコードとプログラムへの引数を -- によって分割する、というようなことをおこなっている。

Ether を送金した人だけコンテンツを閲覧できる Ðapp を書いた

Ethereum はブロックチェーン上でアプリケーションを動かせる(スマートコントラクト)ってので興味を惹かれて、どんなことができるのか調べてたんだけど、感じを掴むために一つ書いてみた。

やりたいことは、ウェブページに送金ボタンがあって、そこから特定のアドレスに Ether を送金し、送金が確認されたら秘密のコンテンツをページ上に表示する、てなもの。送金の確認はスマートコントラクトで行えるが、秘密の情報をブロックチェーン上に記録するわけにはいかないのでこれはウェブサーバに秘匿することになる。とすると、ウェブサーバに私はこの Ethereum アドレスです、とセキュアに伝えてやる必要がある。後で書くけど、あまりいい解法ではない。

知識ゼロの状態から分からないことを潰しつつなんとか動くところまでこれたので、ウェブアプリケーション開発者がつまづいたところをメモっとく。

デモ

まずはデモ。MetaMask で、Ropsten テストネットワークで 0.001 ETH 送金できるアカウントにログインしている必要がある。下のボタンで Heroku のサイトが開くので、そこで(一度も送金していない場合は)Pay する。トランザクションが確定すると Paid になるので、Reveal するとサーバから秘密のコンテンツが送信され、表示されるはず。

**********

ソースコードは https://github.com/motemen/sketch-eth-pay-for-content

MetaMask

MetaMask

で、いきなり MetaMask って何なん、ってなるわけだけど、ウェブと Ethererum ネットワークを仲介するブラウザ拡張なのらしい。Ethereum アカウントの管理や、トランザクションに署名する UI なんかを提供する。ウェブページは JavaScript 経由で MetaMask の API を呼び出して、送金やコントラクトの呼び出しといった、Ethereum 界における処理をおこなうことができる。

Mist という、同じような機能をもつ専用ブラウザみたいなやつもあるんだけど、下に書く認証のための RPC コールが制限されていて、今回は対象としなかった。このようにまだまだインタフェースは整備されておらず、発展途上感がある。

Web3

その JavaScript の API というのが Web3 というライブラリにまとめられている(MetaMask はこの web3 を訪問したページに埋め込む!)。Web3 の API は Ethererum ノードへの RPC 呼び出しとなっている。go-ethereum(geth)のコンソールも Web3 が使える JS の REPL として提供されている。

名前は「Web 3.0」に由来しているっぽい。

構成

ここで構成の話。今回登場するのはひとつのウェブアプリケーションとひとつのスマートコントラクト。

  • スマートコントラクトは別の Ethereum アカウントから Ether の送信を受け取り、いくら受け取ったかを記録しておく(ブロックチェーン上に記録される)。
  • ウェブアプリケーションは秘密のコンテンツを保持しており、訪問者のアカウントがスマートコントラクトに送信した額をチェックして、秘密のコンテンツを訪問者に送信する。

スマートコントラクトのほうのコードはほとんど明らかで、ただ mapping に送信アドレスとその額を保存するだけ。問題はウェブ側となる。

ウェブアプリケーションは訪問者の Ethereum アカウントのアドレスを正しく知る必要がある。訪問者のアドレスは web3 経由で取得することができるが、それを詐称不可能な形で送受信できなくてはいけない。

そこで考えられるのが、クライアントサイドでメッセージへの署名をおこない、サーバサイドでそれを検証する方法。web3 のレイヤではまだ整備されていないが、MetaMask の RPC としてそんなメソッドが用意されている。

The New Secure Way To Sign Data In Your Browser – MetaMask – Medium

ここで説明されているメソッドを呼び出すと以下のような(MetaMask の)ダイアログが表示され、ユーザが「Sign」を選択すると Ethereum アカウントによるメッセージへの署名が生成される。これをサーバが受け取って検証すれば、どのアドレスのアカウントがメッセージに署名したかを知ることができる。

f:id:motemen:20180308003415p:plain

というわけで、全体のフローはこういう感じ。

f:id:motemen:20180308014433p:plain

……のだけれど、署名するメッセージにはウェブサイトに特有の情報が暗黙には含まれないので、悪意あるサイトがユーザに署名させたメッセージを再利用して、アカウントを詐称することができてしまう。そのため上記のようにユーザにアクセスしているサイトを確認するよう促している。ここがけっこう辛いところで、サイトオリジンの情報が署名に付与されるようになると、ウェブからはだいぶ使いやすくなりそう。サービスの利用規約に署名させるなど、メッセージの内容そのものに意味がある場合は問題にならないのだと思う。この話は以下のスレッドでも議論されていた。

Potential vulnerability with web3.personal.sign as authentication mechanism · Issue #1839 · MetaMask/metamask-extension · GitHub

書いてたときには思いつかなかったけど、サイトの訪問者に紐付けられたセッション情報を含んだトランザクションを送信してもらい、それをサーバサイドで検証する、という方法もありそう。セッションごとにトランザクションが必要だけど、そっちのほうが有望かもしれない。そうしたほうがいい気がするな……。

Truffle

Truffle Suite - Your Ethereum Swiss Army Knife

開発の方法は色々あるみたいだけど、Truffle を使った。使ってみた感想としては、これがあれば十分じゃないかな。Solidity によるスマートコントラクトの開発、JavaScript によるテスト、それからデプロイまで Truffle が提供していて、この枠からはみ出したいと思ったことはなかった。作ってるものが小さいからかもしれないけど。

使い方は公式サイトのチュートリアルに詳しく書いてあるので、あまり迷うことはないんじゃないかと思う。マイグレーションやコンソールにおいてコントラクトを表すオブジェクトがどういうインタフェースなのかはよくわからなかったので勘で書いてるけど……。

ウェブとの連携

JavaScript で Ethereum 上のスマートコントラクトとやり取りするのは web3 経由でもできるんだけど、truffle-contract ってモジュールをつかうとより楽になる。

web3 でスマートコントラクトの API を呼び出すには、abi と呼ばれるインタフェース情報とかネットワーク上のアドレスとかが必要なのだけど、truffle migrate でデプロイすると生成される build/contracts/*.json がその情報をすべて保持している。これを truffle-contract に渡せば、スマートコントラクトの API が JavaScript から非常に簡単に叩けるようになる。現在の truffle-contract 3.0.4 では、web3 の 1.0.0-beta とは相容れないので注意。

テスト

テストは JavaScript で書ける。テスト用のネットワークにデプロイしたスマートコントラクトと JavaScript 経由でやり取りするイメージ。mocha 的な書き方ができて、describe の代わりに contract を書くことになっている。テストを書くことで web3 世界観も学べる。

Ganache

Ganache | Truffle Suite

テスト用のプライベートネットワークを GUI で提供するアプリケーション。truffle.json で development ネットワークをこれ(127.0.0.1:7545)に指定すれば、テストやコンソールの実行時に、アカウントやトランザクションの様子をわかりやすく確認できる。デフォルトでは自動マイニングが有効になっていて、発行されたトランザクションがすぐにブロックチェーンに取り込まれるが、マイニングの間隔を指定することもでき、非同期にトランザクションが解決されていく世界でのテストにも便利。

Infura

Infura - Scalable Blockchain Infrastructure

Ethereum ノード(の RPC)を提供するサーバ。普通に Ethereum ネットワークとやりとりすることを考えると、ブロックチェーンのすべてのブロックをダウンロードしないといけないわけだが、Infura が提供する RPC を使えばその手間が省ける。トランザクションへの署名などは手元で行うので、安全(なはず)。サーバサイドでも、コントラクトのデプロイ時にも使っている。MetaMask もこれを使っているらしい。

はまったところ・最初よくわからなかったところ

  • truffle.js と truffle.conf.js が生成されるのは、Windows で truffle を実行しようとしたときに truffle.js が呼び出される挙動の回避策で、どちらかだけ残しておけばよい。
  • Ganache を再起動するとチェーンの内容がリセットされるので、デプロイしたコントラクトも消える。コントラクトへの call は一見成功して見えるのではまった。あらためて truffle migrate --reset する必要がある。
  • Ropsten ネットワークの gas limit は truffle デフォルトの gas limit より低いので、普通にデプロイしようとすると「exceeds block gas limit」というエラーメッセージが出る。gas を明示的に指定してやる必要がある。

所感

これまで中央集権的なアプリケーションしか作ったことがなかったので、簡易なものとはいえ原理的に内部状態がつつぬけのスマートコントラクトを書くのは発想を変えなきゃいけないところがあって頭を使う。スマートコントラクトを使えば権限の表現や操作が適切におこなえそうな感触があった。面白い。

web3 は現在 1.0.0-beta の段階。MetaMask や Mist の足並みも揃っているようには思えないので、Web との自然な連携にはもうちょっと時間がかかりそうだ。だけど Truffle による開発は思った以上に快適だった。

参考文献

ブロックチェーンアプリケーション開発の教科書

ブロックチェーンアプリケーション開発の教科書

ブロックチェーンの歴史から、スマートコントラクトのセキュリティまで広くカバーしていて、とりあえずこの一冊を読んでおけばだいぶ分かる。流れが早い分野だと思うけど、内容も十分新しい。

Ethereum Pet Shop -- Your First Dapp | Truffle Suite

Truffle のチュートリアル。Truffle によるスマートコントラクトの開発全体のオーバービューを知るのによいと思う。

本書について · Ethereum入門

geth でコントラクトの生成をおこなう低レイヤなチュートリアルがある。開発を始めるには Truffle だけでもいいと思うけど、中で何が起きてるか知りたかったらこれを見るとよさそう。

Go 1.10 Release Party in Tokyo で go test, go vet に入った変更の話をしました

Go 1.10 Release Party in Tokyo - connpass

「Go 1.10 ツール周辺の CL を読む」という話をしました。

Go 1.10 ツール周辺の CL を読む - Google スライド

Songmu さんに「モテメン、Goリリースパーティー出ない?」と言われて安請け合いしたはいいものの、とくに話すネタがないのでどうしようかなー、と src/go 以下の変更を読んでいたら普通に面白かったので、そのへんの話をまとめてみました。最初は go test -json の話をしようと考えていたんだけど、go vet 周辺の変更がよかったですね。

最後の方の 1 CL 1 枚のスライドは時間が余ったときのパディングで、発表のときはほぼスキップしました。以下、メインの go vet の話をざっくりまとめておきます。

CL 74356 に至る道

今回の主眼は go test 中にシームレスに go vet を行うようにする、という変更ですが、動機は以下のイシューに端的にまとめられています。

The key insight is that running 'go vet' is considered a best practice, but why is that something people should need to learn and think to do explicitly? If it's such a good practice, it should be integrated into something that happens already.

cmd/compile: gc inconsistent about reporting "unused variable" errors · Issue #8560 · golang/go · GitHub

go vet の実体は go tool vet$GOTOOLDIR/vet) ですが、これは go/* 系の標準パッケージによって構成されている一方で、Go ツールチェインのコンパイルには cmd/compile 以下の特製のソースが利用されている、かつこちらが正であるため、go/* 系、とくに go/types のふるまいを cmd/compile にあわせてやる必要があります(これまで go vet は vendoring や cgo にうまく対応できていないという問題がありました)。

CL 74750: go tool vet に JSON を渡す

そこで、cmd/compile のおこなった import の解決結果をそのまま vet に渡す、という方法が採られました。これまでは .go ソースコードのリストを引数に取っていましたが、vet.cfg という JSON ファイルを引数にあたえることで、vet の解析により詳細な指示を与えられるようになります。go vet -x すると分かるんですが、こんな感じ。

{
        "Compiler": "gc",
        "Dir": "/Users/motemen/dev/go/src/github.com/motemen/prchecklist",
        "GoFiles": [
                "/Users/motemen/dev/go/src/github.com/motemen/prchecklist/context.go",
                "/Users/motemen/dev/go/src/github.com/motemen/prchecklist/models.go",
                "/Users/motemen/dev/go/src/github.com/motemen/prchecklist/version.go",
                "/Users/motemen/dev/go/src/github.com/motemen/prchecklist/context_test.go",
                "/Users/motemen/dev/go/src/github.com/motemen/prchecklist/models_test.go"
        ],
        "ImportMap": {
                "context": "context",
                "fmt": "fmt",
                "github.com/pkg/errors": "github.com/motemen/prchecklist/vendor/github.com/pkg/errors",
                "golang.org/x/oauth2": "github.com/motemen/prchecklist/vendor/golang.org/x/oauth2",
                "net/http": "net/http",
                "net/url": "net/url",
                "testing": "testing"
        },
        "PackageFile": {
                "context": "/usr/local/Cellar/go/1.10/libexec/pkg/darwin_amd64/context.a",
                "fmt": "/usr/local/Cellar/go/1.10/libexec/pkg/darwin_amd64/fmt.a",
                "github.com/motemen/prchecklist/vendor/github.com/pkg/errors": "$WORK/b098/_pkg_.a",
                "github.com/motemen/prchecklist/vendor/golang.org/x/oauth2": "$WORK/b099/_pkg_.a",
                "net/http": "/usr/local/Cellar/go/1.10/libexec/pkg/darwin_amd64/net/http.a",
                "net/url": "/usr/local/Cellar/go/1.10/libexec/pkg/darwin_amd64/net/url.a",
                "testing": "/usr/local/Cellar/go/1.10/libexec/pkg/darwin_amd64/testing.a"
        },
        "ImportPath": "github.com/motemen/prchecklist",
        "SucceedOnTypecheckFailure": false
}
  • GoFiles は解析対象のファイル、
  • ImportMap は import 文に指定されたパッケージパスから実際に解決されたパスへのマップ(.../vendor/... なものが存在してるのが見て取れると思います)、
  • PackageFile は解決されたパッケージパスからビルドされたオブジェクトファイルのパスへのマップ

というふうになってます。go tool vet のほうではこの情報をもとに、ソースコードの型情報の解析の際、import されたパッケージの型情報を解決します。

ちなみに SucceedOnTypecheckFailure は go test 時に true となり、go/types による型チェックが失敗しても go vet を異常終了させないフラグです。go/types にはコーナーケースで型チェックが失敗する既知のバグがあり、これのワークアラウンドになります。go test は build → vet → test という順序で実行されるので vet が走るときにはすでに(cmd/compile による)ビルドが完遂しており、vet による型チェックの失敗はユーザにとって偽の情報となるから無視するわけです。

CL 74354: go/importer.For が lookup 対応

さて、パッケージパスからオブジェクトファイルへの完全なマッピングが得られただけでは go/types の型解析は完全にはなりません。なぜならそういったマッピングに(Go1.9 の段階では)対応していないからです。

go/types では型チェックをおこなう際、import されたパッケージの型情報を得るために go/importer というパッケージを使用します。go/importer

func For(compiler string, lookup Lookup) types.Importer
type Lookup func(path string) (io.ReadCloser, error)

という API を主な機能として提供していて、パッケージパスから型情報を返す types.Importer を生成します。

type Importer interface {
        Import(path string) (*Package, error)
}

lookup はパッケージパスから「ファイル的なもの」を返す関数で、これによってモジュールのオブジェクトの場所をカスタマイズできるのですが、この引数に長らく nil しか許されていなかったのが、このたびサポートされることとなりました。

vet の中ではこれを利用し、コマンドライン引数で受けたマッピングをもとに、パッケージのオブジェクトファイルの場所を cmd/compile 由来のものとしています。

おまけ: cmd/compile vs. go/types

以上の話では cmd/compilego/typesの上位版であるという認識が背景にあったのですが、じつは go/types のほうが型チェックに厳しいところがあって、例えば以下のようなソースコードは、go build はできるものの go/types のチェックを通りません。

https://play.golang.org/p/2BohDi9Mwr8

func f() {
    var x int // x declared but not used
    go func() { x = 42 }()
}

今回の変更で go test 時に vet が走るようになり、このような形のテストはみな失敗することになったわけですが、Go のテストコードはそれにあわせてこのような変数を _ に代入して、このエラーを回避するようになったのでした。おもしろいですね。