Tezos ブロックチェーン のためのスマートコントラクト記述言語 SCaml、チュートリアルの第6回目です。
今回はもう少し複雑は SCaml コントラクトを書いてみます。
- コンセプトの紹介(英語)
- リリースのお知らせ(英語)
- プロジェクトページ
- レポジトリ
- 第0回: コンセプトの説明
- 第1回: 準備
- 第2回: 概観
- 第3回: ウォレット操作
- 第4回: スマートコントラクトのデータ型
- 第5回: はじめての SCaml コントラクト
- 第6回: もう少し複雑なコントラクト。パラメータとストレージ
- 第7回: 投票コントラクトを書いてみる
もうちょい複雑なものを
カウンタ+加算器
- 呼び出されるたびにストレージに 1 足していくカウンタを作ります。
- 今まで与えられた整数引数も別途足し合わせていくことにします。
- パラメタ
- 整数
- ストレージ
- 整数(今までに与えられたパラメタの和) と、 自然数(今までに呼び出された回数)
(* counter.ml *)
open SCaml
let [@entry] main param (param_sum, counter) =
([], (param + param_sum, counter +^ Nat 1))
エントリの第一引数はコントラクトに与えられたパラメタでした。param
という名前で束縛します。
エントリの第二引数はコントラクトに保存されいてるストレージの値でした。今回これは、パラメタの和とカウンタなので、(param_sum, counter)
と二つに分けます。
新しいストレージの値は(param_sum + param, counter +^ Nat 1)
:
- 新しい総和は
param_sum + param
- 新しいカウンタは
counter +^ Nat 1
counter +^ Nat 1
? counter + 1
じゃあねえの? いいえ、違います! SCaml では自然数 Nat «数字»
と書きます。自然数の足し算は +
ではなく +^
です。 うぜぇ!! 理由があります。
Michelson / SCaml の数値型
Tezos のスマートコントラクトには三種類の数値型があります:
- 整数
int
- 多倍長整数です。 SCaml では
Int «数字»
と書きます。Int 42
,Int (-30)
- 自然数
nat
0
から始まる多倍長自然数です。 SCaml ではNat «数字»
。Nat 0
,Nat 69
- トークン
tz
- Tezos のトークンです。 ꜩ1.23 とかああいうやつ。 SCaml では
Tz «正の少数点数»
と書きます。Tz 1.23
。
これらの数値型、特に int
と nat
を判別するために SCaml では敢えて Int «数字»
や Nat «数字»
などと型を明示するようになっています。書き分けはめんどくさいですが…まあすぐ慣れます。
数値型の演算
数値型を操作する演算関数は各数値型それぞれに別の名前で定義されています:
- 整数
int
の演算 +
,-
,*
,/
, ..- 自然数
nat
の演算 (最後に^
がつく。0と0より上**↑**のイメージ) +^
,-^
,*^
,/^
, ..- トークン
tz
の演算 (最後に$
がつく。お金のイメージ) +$
,-$
,*$
,/$
, ..
これはベースとなる OCaml が多重定義を採用していないからです。SCaml では多重定義を許しても良かったんですが、そうすると OCaml とは違う言語になってしまうので相互運用性が犠牲になってしまうんです。
書き分けは面倒ですが、間違っていればコンパイラが型のエラーとしてきちんと報告してくれます。
この他にも色々な数値関連関数があります。SCamlモジュール(リンク)を確認してください。
カウンタのコンパイル、デプロイ、呼び出し
コンパイル
コンパイルは前と同じです:
$ ./scamlc counter.ml
デプロイ
デプロイは… 同じはずなのに、失敗します:
$ ./tezos-client originate contract counter \
transferring 0 from myself \
running counter.tz \
--burn-cap 100
Waiting for the node to be bootstrapped...
Current head: BKrj6qhHwYsp (timestamp: 2022-05-19T09:47:45.000-00:00, validation: 2022-05-19T09:47:46.895-00:00)
Node is bootstrapped.
This simulation failed:
Manager signed operations:
From: tz1Uuf4NLeeEWcX7p7cDCJhPztyoqrdTTuez
Fee to the baker: ꜩ0
Expected counter: 10593033
Gas limit: 1040000
Storage limit: 60000 bytes
Origination:
From: tz1Uuf4NLeeEWcX7p7cDCJhPztyoqrdTTuez
Credit: ꜩ0
Script:
{ parameter int ;
storage (pair int nat) ;
code { UNPAIR ;
SWAP ;
UNPAIR ;
PUSH nat 1 ;
DIG 2 ;
ADD ;
SWAP ;
DIG 2 ;
ADD ;
PAIR ;
NIL operation ;
PAIR } }
Initial storage: Unit
No delegate for this contract
This operation FAILED.
Ill typed data: 1: Unit is not an expression of type pair int nat
At line 1 characters 0 to 4, value Unit is invalid for type pair int nat.
At line 1 characters 0 to 4,
invalid primitive Unit, only Pair can be used here.
Fatal error:
origination simulation failed
Script:
から始まるスマートコントラクトの情報:
Initial storage: Unit
と、最後のエラーメッセージを見てください:
Ill typed data: 1: Unit is not an expression of type pair int nat
At line 1 characters 0 to 4, value Unit is invalid for type pair int nat.
At line 1 characters 0 to 4,
invalid primitive Unit, only Pair can be used here.
Fatal error:
origination simulation failed
Tezos は Michelson のコードしか知らないので、エラーも Michelson のコードについての物になります。わかりにくいんですが、これは storage が (int * nat)
なのに、初期ストレージの値として Unit
(()
) が使われているので怒られています。
--init
で初期ストレージを指定する
./tezos-client originate --help
を見るとわかりますが、初期ストレージは --init «Michelsonの値»
で指定します。このオプションが省略されると Unit
(SCaml でいう ()
) が使われます。上の例ではそのため、int
が必要なのにUnit
が与えられたと言っているわけですね。
適切な初期ストレージを与えましょう。総和もカウンタも 0 なので、初期値はSCaml では (Int 0, Nat 0)
ですね。(そうです、(0, 0)
ではなく (Int 0
, Nat 0)
)。でも Tezos は SCaml を理解しません。これを Michelson の値に変換してやらなければいけません。これは… とりあえず、天下り的に書くと… Pair 0 0
となります。:
$ ./tezos-client originate contract counter \
transferring 0 from myself \
running counter.tz \
--init 'Pair 0 0' \
--burn-cap 100
Waiting for the node to be bootstrapped before injection...
...
New contract KT1XHVD2arZ2MynmoJ1Zqn9ZkNCUEDcH7fjJ originated.
...
Contract memorized as counter.
できました!
SCaml の値から Michelson の値を求める: scamlc --scaml-convert
先ほど、SCaml の(Int 0, Nat 0)
は Michelson のPair 0 0
だと言いましたが、いちいち人間が手で変換するのも面倒です。自動的にやる方法があります。次のファイルを作って:
(* counter_init.ml *)
open SCaml
let init = (Int 0, Nat 0)
これを --scaml-convert
フラッグをつけてコンパイルします:
$ ./scamlc --scaml-convert counter_init.ml
Nothing to link...
init: Pair 0 0
すると init
で指定した SCaml の値は Michelson では Pair 0 0
になることを教えてくれます。
注意: --scaml-convert
できるのは定数式のみです。
呼び出し
呼び出しは ./tezos-client transfer ...
でした。前と同じようには…やはりいきません。
$ ./tezos-client transfer 0 from myself to counter
Waiting for the node to be bootstrapped...
Current head: BMYygDeP4w7W (timestamp: 2022-05-19T09:51:20.000-00:00, validation: 2022-05-19T09:51:33.981-00:00)
Node is bootstrapped.
Change detected, rebuilding site.
2022-05-19 18:51:36.139 +0900
Source changed "/Users/jun/dailambda.gitlab.io/content/blog/2020-06-15-scaml-jp-6/index.md": WRITE
WARN 2022/05/19 18:51:36 Page.Hugo is deprecated and will be removed in a future release. Use the global hugo function.
Total in 31 ms
This simulation failed:
Manager signed operations:
From: tz1Uuf4NLeeEWcX7p7cDCJhPztyoqrdTTuez
Fee to the baker: ꜩ0
Expected counter: 10593034
Gas limit: 1040000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ0
From: tz1Uuf4NLeeEWcX7p7cDCJhPztyoqrdTTuez
To: KT1XHVD2arZ2MynmoJ1Zqn9ZkNCUEDcH7fjJ
This operation FAILED.
Invalid argument passed to contract KT1XHVD2arZ2MynmoJ1Zqn9ZkNCUEDcH7fjJ.
At (unshown) location 0, value Unit is invalid for type int.
At (unshown) location 0, unexpected primitive, only an int can be used here.
Fatal error:
transfer simulation failed
最後のエラーを見ると、不正な引数を渡している。int
のはずなのに Unit
(()
) を渡しただろう、と言っています。引数を与えなければいけないです。
--arg
で渡すパラメタを指定する
今回のコントラクトはパラメタが ()
ではないので、呼び出し時にパラメタを指定してやらなくてはいけません。 ./tezos-client transfer .. --arg «Michelsonの値»
を使います。 --arg 42
としてみましょう:
$ ./tezos-client transfer 0 from myself to counter --arg 42
...
Transaction:
Amount: ꜩ0
From: tz1Uuf4NLeeEWcX7p7cDCJhPztyoqrdTTuez
To: KT1XHVD2arZ2MynmoJ1Zqn9ZkNCUEDcH7fjJ
Parameter: 42
This transaction was successfully applied
Updated storage: (Pair 42 1)
Storage size: 72 bytes
Consumed gas: 2059.285
パラメタとして 42
が与えられ、新しいストレージの値が Pair 42 1
になったとあります。これは SCaml でいう (Int 42, Nat 1)
です。上手く動きました!別のパラメタを使って繰り返し呼んでみてください。
え?動かなかった?--burn-cap
とか言われた? もし、すごく大きな値(420000000000
とか)を入れると、そのままでは動かず、storage burn を要求されます。これは大きな整数を格納するためにブロックチェーン上に記憶容量が必要で、その費用を支払わなければいけないからです。指定された--burn-cap «値»
をつければ動くはずです。
もう皆さんお気づきとは思いますが、Tezos ブロックチェーンはデータベースとして見ると遅いですね。これはアップデートの最小単位であるブロックが作られる間隔である block time が30秒と非常に長いためです。
まとめ
パラメタとストレージを使う時:
- デプロイには
--init «初期値»
- 呼び出しには
--arg «パラメタ»
- SCaml の値ではなく、Michelson の値を使う。変換は
./scamlc --scaml-convert
- ストレージが大きくなる際には費用を支払わなければならない。
--burn-cap
を使う。
状態確認にブロックエクスプローラーをつかう
コントラクトの状態確認(ストレージなど)は ./tezos-client
を使ってちまちまコントラクトのストレージにアクセスすればわかるのですが、めんどくさいです。ブロックエクスプローラを使えば簡単に確認できます。
TzStat でもいいのですが、スマートコントラクトの状態を調べるのには Better Call Devがオススメです。カウンタの例を見ると、どういうコードがデプロイされ、呼び出しでストレージがどうアップデートされたかがよくわかります:
高級言語も複数あるし、ブロックエクスプローラも複数あるし、どうなってんねん、どれが正式やねん!! と思われるかもしれませんが、正式なものが唯一ある、と使う側は気楽かもしれませんが、クリティカルシステムである Tezos では好まれません。常に複数の手段を用意しておき、一つがおかしくなったら他を使えるようにあえてしています。
これで基本はできました!!
複雑なプログラミングに入る前に、普通のプログラミングにはない、ブロックチェーン特有な事情を理解する必要がありましたが、これで大体抑えることができました。次からはもっと SCaml よりの話題になります。