以前の記事で Tezos の説明を書いた際に、スマートコントラクトについて言及した。
このスマートコントラクトという機能が Tezos 等の暗号通貨の非常に大きな特徴だと思うので、是非ともこれを触ってみたい。
なので、この記事では Tezos でスマートコントラクトを取り扱ってみたいと思う。
Tezos とスマートコントラクト
スマートコントラクトというのは、暗号通貨のネットワーク上に登録されて実行されるプログラムで、どういう条件で何が実行されるのか、誰もが見ることができるという特徴がある。
1つのスマートコントラクトが1つのアカウントみたいに扱われ、スマートコントラクトに対して Tezos の送金を行う事ができる。どういうコードが実行されるか読むことができるので、全て納得した上で送金する事ができるというわけだ。スマートコントラクトのプログラムに従って、スマートコントラクトから他のアカウントに送金したりもできるらしい。
以前も書いたけど、イーサリアムとかにもこの機能はあるし、この機能を使ってイーサリアム上に他の暗号通貨を構築するなどで使われているらしい。
で、 Tezos のスマートコントラクトの特徴なのだけど、スマートコントラクトを記述するのに Michelson という言語(かつ、その言語が実行される VM の名前でもある)を用いる点にある。これは厳格な静的型を持つスタックベースの言語なのだけど、「静的型付き」と強調した通り、型検査によりバグの混入を抑えられるというわけだ。
この辺りの特徴は、流石に作ってる人達が関数型勢なだけはあるなぁと感じるところ。
スマートコントラクトとプログラミング言語
それで、じゃあスマートコントラクトを作成するのにその Michelson を書くかという話なのだが、流石にスタックベースの言語は独特で、直接記述するのは辛いものがある。
Tezos 開発側ももちろんそれを把握しているので、イーサリアムの Solidity みたいに、 Michelson にコンパイルされる高級言語を公式で用意している。 LIGO という言語がそれ。
ちょっと面白い言語で、なんと表層構文が4種類(!?)も用意されている。 JS 風、 OCaml 風、 Pascal 風、 Reason 風らしい。
わざわざ Pascal 風が用意してあったり、 JS 風と Reason 風が両方とも用意してあったりと、かなり開発者の癖が出ている興味深いチョイスだと思う。
ただこの「風」というのが曲者で、結局の所その元の言語ではないので、ライブラリなどの資源が流用できなかったり、コーディングの上でも無数の落とし穴が自ずと生まれたりして、中々思い通りに使えないものだったりする。
LIGO の他にも、 Python で開発できる SmartPy というやつもあるらしい。
要するに Python コードを Michelson に変換してくれるそうなのだが、公式サイトによると、まず Python を OCaml ベースの SmartML というのに変換してから、それを Michelson に変換するらしい。
単なる言語ではなくて、 Python でスマートコントラクトを開発するためのツールセットで、テストスイートや IDE までも含んでいると書いてある。なるほどとても便利そうで、特にテスト環境が整っている点は非常に魅力的なのだけど、 Python を書かないといけないのが何だかなぁという気分にはなる。 Michelson は静的型付き言語なのに。
どうせ途中で OCaml に変換するのなら、OCaml を直接書かせてくれれば良いのに……。
というわけで、そういう言語を使っていくことにする。
SCaml
SCaml もまた OCaml ベースの言語で Michelson にコンパイルされるのだが、特徴として OCaml の完全なサブセットである点が挙げられる。
つまり、上に書いた「○○風」の落とし穴が存在せず、かつ OCaml というよく出来た静的型付き言語の機能を縦横に利用できるわけだ。
これだよ、求めていたものは! という感じである。
開発しているのはダイラムダ株式会社の古瀬さん。 Tezos のコア開発にも参画されている。
公式ブログで SCaml の紹介とチュートリアルの記事も公開されている。
このチュートリアルがとても分かりやすいので、この記事通りに進めていきたいと思う。
Tezos の準備を行う
以前の記事で行ったのと同じように、 Tezos を使う為の準備を行う。
前は opam を利用してコンパイルしたけど、今回はバイナリを落としてきてそれを使ってみようかと思う。どうせテストネットにしか接続しないつもりなので。
普通にバイナリを落としてきて、権限を変えてからバージョンを確認。
$ ./tezos-client --version
4ca33194 (2022-08-01 11:55:43 +0200) (14.0)
$ ./tezos-node --version
4ca33194 (2022-08-01 11:55:43 +0200) (14.0)
ヨシ!
次にテストネットに繋いでいく。
テストネットは、バージョンが固定されたものには現行のプロトコルと最新のプロトコルとの2つが提供されているのだが、それとは別にバージョンが固定されない Ghostnet というものがある。
これは長期運用の為のテストネットで、メインネットと同じようにプロトコルがアップデートされていく。プロトコルが固定されたテストネットだと、各プロトコル固有のテストはできても、所謂ステージング環境のような長く利用する運用が行い難いので(プロトコルのバージョンが切り替わる度に環境がリセットされてしまうので)、そういう場合はこちらを利用すると良さそうだ。
今回はその ghostnet を使ってみる。
$ ./tezos-node config init --data-dir ./.tezos-node-ghostnet --network ghostnet
Aug 24 03:15:38.173 - node.config.validation: the node configuration has been successfully validated.
Created ./.tezos-node-ghostnet/config.json for network: ghostnet.
$ ./tezos-node identity generate --data-dir ./.tezos-node-ghostnet
Generating a new identity... (level: 26.00)
Stored the new identity (idtw1wcubuCe1p4s5LewrADTCB2WS6) into './.tezos-node-ghostnet/identity.json'.
良さそうである。
全部の取引データを持ってきていてはいつまでかかるか分からないので、以前やったようにスナップショットを利用する。
$ wget https://ghostnet.xtz-shots.io/ghostnet-1043996.rolling
$ ./tezos-node snapshot import ghostnet-1043996.rolling --block BM7mXRYL1zD7KxbzupzYmFvzYeXVrxRvszxp1J7k58R5oqhBLao --data-dir ./.tezos-node-ghostnet
Aug 24 03:20:53.785 - node.snapshots: importing data from snapshot
Aug 24 03:20:53.785 - node.snapshots: ghostnet-1043996.rolling: chain TEZOS_ITHACANET_2022-01-25T15:00:00Z, block hash BM7mXRYL1zD7KxbzupzYmFvzYeXVrxRvszxp1J7k58R5oqhBLao at level 1043996, timestamp 2022-08-20T01:22:35-00:00 in rolling (snapshot version 4)
Aug 24 03:20:53.785 - node.snapshots: retrieving and validating data. This can take a while, please bear with us
Writing context: 1786K/1786K (100%) elements, 369MiB read Done
Copying protocols: 1/1 Done
Storing floating blocks: 120 blocks written Done
Aug 24 03:21:23.256 - node.snapshots: successful import from file ghostnet-1043996.rolling
読み込めた。
ではノードを立ち上げてみよう。
$ ./tezos-node run --data-dir ./.tezos-node-ghostnet --rpc-addr 127.0.0.1
クライアント側でも準備をする。
$ ./tezos-client bootstrapped
スナップショットを使ってすら、そこそこ時間がかかる。
timestamp が徐々に現時点に近付いてくるのを楽しみながら、ゆっくりと待つ。
Node is bootstrapped.
と言われれば準備完了である。
さて、準備が終わったら無料の Tezos を受け取っておこう。テストネットでは大量の Tezos を無料でもらう事ができる。残念なのは換金が不可能な点のみである。
$ ./tezos-client activate account faucet with ./ghostnet.json
$ ./tezos-client get balance for faucet
Warning:
This is NOT the Tezos Mainnet.
Do NOT use your fundraiser keys on this network.
76461.899059 ꜩ
良さそう。
(この「今使ってるのはメインネットじゃないから気をつけろよ」という警告は毎回出てくるので、以下省略する。)
SCaml の準備を行う
基本的にダイラムダさんのブログに書いてある手順をこなしていくだけである。
$ git clone https://gitlab.com/dailambda/docker-tezos-hands-on
$ cd docker-tezos-hands-on
$ ./check
これでエラーが出なければ良さそうである。
では、最小のプログラムを書いてコンパイルしてみる。
open SCaml
let [@entry] main () () = [], ()
$ ./scamlc simplest.ml
Linking Simplest
Compiling simplest Simplest_0
コンパイルできた!
simplest.tz というファイルに吐き出されている。
$ cat simplest.tz
parameter unit ;
storage unit ;
code { DROP # let param_storage_164 = (input, storage)
# free param_storage_164
; UNIT # let _unit_181 = ()
; NIL operation # let _ops_182 = []
; PAIR # let _pair_183 = (_ops_182, _unit_181)
# return _pair_183
}
Michelson にコンパイルされている!
テストネットにデプロイする
この最小のスマートコントラクトを、いよいよテストネットに登録してみよう。
これもダイラムダさんのブログ記事通りに。
./tezos-client originate contract simplest \
transferring 0 from faucet \
running docker-tezos-hands-on/simplest.tz \
--burn-cap 100
Node is bootstrapped.
Estimated storage: no bytes added
Estimated gas: 1408.922 units (will add 100 for safety)
Estimated storage: 297 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'onhryTUhoZXwASV2VnzVmJSJtw2jUMBzLWNDZDDEnUSaG6RGQhJ'
Waiting for the operation to be included...
Operation found in block: BMKhWjgPPQhCoR6JriyjKXfgX5NpSjzE2Rg5p11Hwqaz7skj4Wt (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.00036
Expected counter: 11693406
Gas limit: 1000
Storage limit: 0 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.00036
payload fees(the block proposer) ....... +ꜩ0.00036
Revelation of manager public key:
Contract: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Key: edpkuNWJjP8jW56BDKSrMhiB5bJraJpUpu59ux4KgJketjrgnv7nzF
This revelation was successfully applied
Consumed gas: 1000
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.000325
Expected counter: 11693407
Gas limit: 1509
Storage limit: 317 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000325
payload fees(the block proposer) ....... +ꜩ0.000325
Origination:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Credit: ꜩ0
Script:
{ parameter unit ;
storage unit ;
code { DROP ; UNIT ; NIL operation ; PAIR } }
Initial storage: Unit
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW
Storage size: 40 bytes
Paid storage size diff: 40 bytes
Consumed gas: 1408.922
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.01
storage fees ........................... +ꜩ0.01
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.06425
storage fees ........................... +ꜩ0.06425
New contract KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for onhryTUhoZXwASV2VnzVmJSJtw2jUMBzLWNDZDDEnUSaG6RGQhJ to be included --confirmations 1 --branch BLyhvn8r3NnUVpPvBi5ZC9gWCG5qkmg5WxC7zmw8NZra1hNXvK5
and/or an external block explorer.
Contract memorized as simplest.
登録できたようだ。
呼び出してみよう。
$ ./tezos-client transfer 0 from faucet to simplest
Node is bootstrapped.
Estimated gas: 2110.410 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ooDSpJ7trNCPvBLV1UGpVwEeDD2nhkJmC46V5oqsTvViTC3GKAn'
Waiting for the operation to be included...
Operation found in block: BMUmSRo48coPJ61zv8irhvWnebBTTDGtzQKTniPtmTzgqzN8Z7w (pass: 3, offset: 1)
This sequence of operations was run:
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.000473
Expected counter: 11693408
Gas limit: 2211
Storage limit: 0 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000473
payload fees(the block proposer) ....... +ꜩ0.000473
Transaction:
Amount: ꜩ0
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
To: KT1MuUCgjWF1wMBGBVsLQE3prKV3Tyu6vwFW
This transaction was successfully applied
Updated storage: Unit
Storage size: 40 bytes
Consumed gas: 2110.410
期待通りの挙動(何もしない)をしている。
これで、自作のスマートコントラクトがネットワークに登録できたわけだ。
もう少し多機能なスマートコントラクトを書いてみる
これだと余りにも機能が少なくて寂しいので、もう少し機能のあるスマートコントラクトを作ってみよう。
上述した SmartPy のサイトに載っているサンプルを SCaml で実装してみたいと思う。
このスマートコントラクトが持つ機能はこうだ。
- ストレージには数値を1つ持つ
- replace エントリーポイントは、ストレージを与えられた数値で置き換える
- double エントリーポイントは、ストレージの数値を2倍にする
SmartPy のコードではこのようになっている。
import smartpy as sp
class StoreValue(sp.Contract):
def __init__(self, value):
self.init(storedValue = value)
@sp.entry_point
def replace(self, value):
self.data.storedValue = value
@sp.entry_point
def double(self):
self.data.storedValue *= 2
では作ってみよう。
まず SCaml のコードを書く。
open SCaml
let [@entry] replace value _current = [], value
let [@entry] double () current = [], (current * Int 2)
複雑性が増したと言っても、機能は僅か1行である。
SCaml の詳しい説明はダイラムダさんのブログ記事に譲るが、コードの見所は次のような感じ。
- エントリーポイントの関数は引数を2つ取り、最初の引数が呼び出し時に与えられたパラメータ、次の引数がストレージの中身
- エントリーポイントの関数が返すのは「オペレーション」と「ストレージ」のタプル
- この時、新しいストレージを返すことでストレージを更新することができる
- Michelson で利用される数値は整数、自然数、トークンの3種類なので、計算を行う場合は型を合わせなければいけない
- 逆に言えば、「トークンに整数を加算」するような誤ったコードはコンパイル時に弾かれる
このコードを Michelson にコンパイルする。
$ ./scamlc example.ml
吐き出された Michelson ファイルを、 SmartPy のサイトに載っている Michelson コードと見比べてみると面白い。
コンパイラが違うので、全く同じコードにはなっていないが、結果的には同じ機能を持つはずだ。
ではこの Michelson コードをテストネットにデプロイする。
./tezos-client originate contract example \
transferring 0 from faucet \
running docker-tezos-hands-on/example.tz \
--init '0' \
--burn-cap 100
Node is bootstrapped.
Estimated gas: 1421.297 units (will add 100 for safety)
Estimated storage: 356 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'oofJqpkA7X4YK8dDbXGdN9Y8GxRNYFS1EZH7sM8SdGeDQ3pLsAq'
Waiting for the operation to be included...
Operation found in block: BLmnszL4LCKWVeHxfBwvvGaNhciXDNeEieiog9E9MebJRc3d1Gv (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.000482
Expected counter: 11693409
Gas limit: 1522
Storage limit: 376 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000482
payload fees(the block proposer) ....... +ꜩ0.000482
Origination:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Credit: ꜩ0
Script:
{ parameter (or (int %replace) (unit %double)) ;
storage int ;
code { UNPAIR ;
IF_LEFT
{ SWAP ; DROP ; NIL operation ; PAIR }
{ DROP ; PUSH int 2 ; SWAP ; MUL ; NIL operation ; PAIR } } }
Initial storage: 0
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
Storage size: 99 bytes
Paid storage size diff: 99 bytes
Consumed gas: 1421.297
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.02475
storage fees ........................... +ꜩ0.02475
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.06425
storage fees ........................... +ꜩ0.06425
New contract KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7 originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for oofJqpkA7X4YK8dDbXGdN9Y8GxRNYFS1EZH7sM8SdGeDQ3pLsAq to be included --confirmations 1 --branch BM82aPJLxBtZvjtaJJT3Tg1KTmjR39CSdXYPBXaBqyQQ8BXRQ44
and/or an external block explorer.
Contract memorized as example.
宜しい。
Initial storage: 0
なので、ストレージが 0
で初期化されていることが分かるだろう。
よく見ると表示されている Michelson コードが渡したものとちょっと違うが、コメントを取り除いたりするだけじゃなく簡単な最適化も行ってくれているのだろうか。
それでは、呼び出してみよう。
複数のエントリーポイントを持つスマートコントラクトの場合、 --entrypoint
でどのエントリーポイントを呼び出すのかを指定する。
$ ./tezos-client transfer 0 from faucet to example --entrypoint replace --arg 42 --burn-cap 100
Node is bootstrapped.
Estimated gas: 2119.147 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'op5yNxoRrgAyMbk6VBajdZpymyhtvupvWmWTbDbGUqUdHW7KCwC'
Waiting for the operation to be included...
Operation found in block: BMeYHtMavEBVXWaThikoThkBzv7mySMpzdTFxYbDuhyt2B1DoZn (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.000488
Expected counter: 11693410
Gas limit: 2220
Storage limit: 0 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000488
payload fees(the block proposer) ....... +ꜩ0.000488
Transaction:
Amount: ꜩ0
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
To: KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
Entrypoint: replace
Parameter: 42
This transaction was successfully applied
Updated storage: 42
Storage size: 99 bytes
Consumed gas: 2119.147
処理が成功し、ストレージが渡した引数である 42
で置き換えられた事が伝えられている。
では、もう片方のエントリーポイントも呼び出してみよう。
こちらは引数不要だ。
$ ./tezos-client transfer 0 from faucet to example --entrypoint double --burn-cap 100
Node is bootstrapped.
Estimated gas: 1203.625 units (will add 100 for safety)
Estimated storage: 1 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'opPCwkLRp7RgUTDa38Wca71nk7Cw1vFLL161aB93tEdfhAxT3fA'
Waiting for the operation to be included...
Operation found in block: BMUbjipj2cbaNfnKJ7PcEZRQSZ2QFrgWmCbmrkXxdnVGyr8ncgS (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
Fee to the baker: ꜩ0.000396
Expected counter: 11693411
Gas limit: 1304
Storage limit: 21 bytes
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.000396
payload fees(the block proposer) ....... +ꜩ0.000396
Transaction:
Amount: ꜩ0
From: tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC
To: KT1Uy3VGP9uWKfY8FR1NagDWhaCCnBe9gVf7
Entrypoint: double
This transaction was successfully applied
Updated storage: 84
Storage size: 100 bytes
Paid storage size diff: 1 bytes
Consumed gas: 1203.625
Balance updates:
tz1YESy2MnxA2UzxCpJohJB6znSVyRby98NC ... -ꜩ0.00025
storage fees ........................... +ꜩ0.00025
同様に成功して、ストレージが 42
の2倍の 84
で置き換えられている。
こんな感じで、多少の機能を持ったスマートコントラクトでも簡単に開発、デプロイ、呼び出しができる。
まとめ
Tezos のスマートコントラクトを記述し、デプロイすることができた。
思ったより簡単だったな、というのがやってみた感想かな。
とはいえ今回作ったスマートコントラクトはどれも非常に単純な、遊びのようなものなので、もう少し面白いスマートコントラクトを作ってみたいところ。