Facebook libraのMove言語でスマートコントラクトを書いてみる
こんにちは。Fintertechストラテジーグループの恩田です。
自己紹介
以前はSIerでネットワークエンジニアとして証券系システムの設計、構築を担当していました。2018/10よりFintertechに入社し、新規事業の企画とエンジニアしてます。以前はインフラばかりでしたが、最近はフロントエンドも頑張ってます。
個人プロジェクトでは、自然言語処理や確率的データ構造、バイナリフォーマット(Protocol Buffersなど)をいじったりしています。主にGolangを書いていますが、ここ1~2年くらいはRustもやってます。最近はMonkey言語のRust実装を書いているので、libraの実装も読みやすかったです。
はじめに
先日、川浪がlibraに関する記事を投稿しました。
libraでは、Moveという言語でスマートコントラクトを実装します。本記事の執筆時点ではstableではなく、Move IRと呼称されています。Moveは型付きのバイトコード言語で、ドキュメントにも安全で柔軟な言語と述べられています。また、Move自体がRustで実装されており、コンパイラやVMの実装も公開されています。
本日はlibraのスマートコントラクト言語であるMove IRで簡単なスマートコントラクトを実装してみます。チュートリアルのGetting Started With Moveには、サンプルコードが書かれていますが、実行するところまでは書かれていません。今回はもう一歩踏み込み、テストまで実装します。
環境、前提
libraをgithubからclone、セットアップ済みであることを前提とします。
なお本記事では、libraレポジトリを$HOME直下にcloneした前提でPATHを表現しています。また、Move IRはまだstableではないため、#bd8e6dcのコミットでの実装となります。
実際、チュートリアルにも以下のように書かれており、現時点ではテストネットにデプロイすることはできないようです。try out locallyというのが今回実装するテストまでのことを指していると思われます。
Custom Move programs are not supported in the initial testnet release, but these features are available for you to try out locally.
実装するもの
touchした回数をGlobal Stateとして記録するカウンターをスマートコントラクトとして実装します。テストがあったほうが動作がわかりやすいと思うので、先にテストを書いてしまいましょう。
main() {
let sender: address;
let count: u64;
sender = get_txn_sender();
// 初期化。この時点では、countの値は`0`
Counter.publish();
count = Counter.get_count(copy(sender));
assert(copy(count) == 0, 0);
// 一度目の`touch`。countの値が`1`となる。
Counter.touch();
count = Counter.get_count(copy(sender));
assert(copy(count) == 1, 0);
// 二度目の`touch`。countの値が`2`となる。
Counter.touch();
count = Counter.get_count(copy(sender));
assert(copy(count) == 2, 0);
return;
}
このテストを通すことがゴールです。
実装
Move IRのソースコードは~/libra/language/functional_tests/tests/testsuite/下に配置します。
拡張子は.mvirです。まずはディレクトリ、ファイルを用意します。
❯ mkdir ~/libra/language/functional_tests/tests/testsuite/counter
❯ touch ~/libra/language/functional_tests/tests/testsuite/counter/counter.mvir
このcounter.mvirに実装していきます。
modules:
module Counter {
// Counterモジュールは内部にtouchされた回数をresourceとして記録する。
resource T {
count: u64,
}
// resourceを初期化し、publishする関数
public publish() {
// 組み込み関数move_to_senderでGlobal Stateを更新する
move_to_sender<T>(T{ count: 0 });
return;
}
// 現在のカウンター値を取得する関数
public get_count(sender: address): u64 {
let counter: &mut R#Self.T;
let count: u64;
// 組み込み関数borrow_globalでGlobal Stateを取得
counter = borrow_global<T>(move(sender));
count = *(&move(counter).count);
return move(count);
}
public touch() {
let sender: address;
let counter: &mut R#Self.T;
let count: u64;
sender = get_txn_sender();
// mutableなGlobal Stateを借用し、カウンター値をインクリメントする
counter = borrow_global<T>(move(sender));
count = *(©(counter).count);
*(&mut move(counter).count) = move(count) + 1;
return;
}
}
script:
import Transaction.Counter;
// 以下、テストの再掲。
main() {
let sender: address;
let count: u64;
sender = get_txn_sender();
Counter.publish();
count = Counter.get_count(copy(sender));
assert(copy(count) == 0, 0);
Counter.touch();
count = Counter.get_count(copy(sender));
assert(copy(count) == 1, 0);
Counter.touch();
count = Counter.get_count(copy(sender));
assert(copy(count) == 2, 0);
return;
}
テスト結果
❯ cargo test -p functional_tests counter
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running ~/libra/target/debug/deps/functional_tests-366ed9b3ef272af3
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 7 filtered out
Running ~/libra/target/debug/deps/testsuite-fa4b47129fb00c0f
running 1 test
test functional_tests::counter/counter.mvir ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 226 filtered out
所感
・Rustをラップしていると感じる場面が多いので、Rustの経験があれば違和感は少なそう。
・テストを通すのに結構苦労した。Runtimeの前にVerifierでチェックしてくれるので安全にスマートコントラクトが書けそう。
・すべての変数宣言を冒頭でしなければならない。使う場所で宣言したい。
・moveとcopyをいちいち書くのがめんどくさい。
・組み込みでどんな関数があるのか、実装を確認しないといけない(これは将来的にドキュメントが整備されれば解決されると思う)。
・シンタックスハイライトやコード補完のプラグイン対応がないので現時点では開発者体験が良くなかった(これもすぐ解決するはず)。
・ホワイトリストでアドレスに制限を掛けたり、異常系の検証をしてみたい。