SCamlによるTezosプログラミング#6

Tezos ブロックチェーン のためのスマートコントラクト記述言語 SCaml、チュートリアルの第6回目です。

今回はもう少し複雑は SCaml コントラクトを書いてみます。

もうちょい複雑なものを

カウンタ+加算器

  • 呼び出されるたびにストレージに 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

これらの数値型、特に intnat を判別するために 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がオススメです。カウンタの例を見ると、どういうコードがデプロイされ、呼び出しでストレージがどうアップデートされたかがよくわかります:

https://gitlab.com/dailambda/images/-/raw/master/better-call-dev.png

高級言語も複数あるし、ブロックエクスプローラも複数あるし、どうなってんねん、どれが正式やねん!! と思われるかもしれませんが、正式なものが唯一ある、と使う側は気楽かもしれませんが、クリティカルシステムである Tezos では好まれません。常に複数の手段を用意しておき、一つがおかしくなったら他を使えるようにあえてしています。

これで基本はできました!!

複雑なプログラミングに入る前に、普通のプログラミングにはない、ブロックチェーン特有な事情を理解する必要がありましたが、これで大体抑えることができました。次からはもっと SCaml よりの話題になります。