ブロックチェーンでどんな DApps を作るか
何でもいいです、できたら:
- 普通の中央集権 DB ではできない事
- 暗号通貨でのやりとり (本気でやると税とか大変だけど
- オープンになっていることの魅力
- ブロックチェーンやネットの外の世界と繋がっていると面白い
ꜩ 今回はほぼ Tezos の話です
なのでマークつけません
Tezos ブロックチェーンの基本
- 普通の DB と比べれば遅い: 更新単位時間 15秒
- 記憶域は高い: 1byte = 0.04円
- 各アカウントはトークンを保持しており、使用料はそこから支払われる
- 送金手数料、記憶域確保手数料
- 公開鍵暗号によるアカウント認証
- 秘密鍵を知っている人がオーナー
- 公開鍵はチェーン上に公開 (Reveal操作)
- アカウント操作が正当であることを公開鍵で検証
- スマートコントラクト: 特殊なアカウント。
KT1...
- トークン保有量の他に、コントラクトコード、データを持つ
- 送金をトリガーにコントラクトコードを実行
Tezos でのチェーン操作の流れ
- ユーザー/ が送金などの op を作る
- 秘密鍵を持つウォレットが op に署名
- ウォレットが署名付き op を mempool に入れる
- バリデータが mempool 内の ops を取り上げブロックを作成
- バリデータがブロックをチェーンの先頭に登録
- バリデータ達が新ブロックに裏書き(endorsing)投票を行う
- ブロックが確定
DApps の構造
- On chain: スマートコントラクト
- 金銭取引、非中央集権性、認証、改竄不可能性を提供
- Off chain: ブロックチェーンが提供しない部分
- デスクトップ、スマホ、ブラウザ アプリや、補助 DB など
- ウォレット
- Off chain プログラムがチェーンに対して行いたい操作に対し公開鍵署名を行う
Tezos DApps の流れ
- DApps はRPCで情報を読み取る
- ユーザーからの操作を受け、スマートコントラクト呼び出しパラメータを決定
- DApps は op をユーザーウォレットに送る
- ウォレットはユーザーの秘密鍵で署名を行い、チェーンに送る
- スマートコントラクトは op のパラメータを受け取ってコードを実行する
- DApps はブロックチェーンに op が取り込まれたのを確認する
Tezos での DApps 開発
- ウォレット (Kukai, Temple など)
- テストネット
- ブロックエクスプローラ
- Tezos スマートコントラクトの構造
- スマートコントラクト記述とデプロイ (SmartPyなど)
- オフチェーンプログラミング (Taquitoなど)
ドキュメント
いろいろありますが、公式のものが一番良い:
- Tezos Developer Portal: 入門
- より開発者向けの情報: http://tezos.gitlab.io/
今日扱うツール関連
全て英語です。キツイ人は自動翻訳使ってください。
ウォレットを変えます
すいません、Kukai wallet は使いにくすぎるので、
Temple ウォレット をインストールしてください
- 演習
- Temple ウォレットのインストール
Temple ウォレットのインストール
Safari は使えません。 Chrome か Firefox を使ってください。
- https://templewallet.com/download?platform=extension
- 使っているブラウザの INSTALL を押す。
- 拡張機能をインストール。
私の授業の課題が終わったらこの拡張を消して構いません。
Temple ウォレットのインストール
Safari は使えません。 Chrome か Firefox を使ってください。
- https://templewallet.com/download?platform=extension
- 使っているブラウザの INSTALL を押す
- 拡張機能をインストール
Temple ウォレットの初期化
前回作ったアカウントを復活させる
- Import existing Wallet
Temple ウォレットの初期化
- 前回作ったアカウントのシードフレーズを入力
全部をコピーして、#1 にペーストすると一度に埋めてくれる
- Next
Temple ウォレットの初期化
- Password を入力(2回)
- Accept terms にチェック。 Skip Onboarding にチェック。
- Import
同じアカウントが回復できるはず
Temple: Ghostnet を選ぶ
Mainnet での残高は 0 tez。
Ghostnet に切り替えれば残高が見えるはず。
Temple: サブアカウントを作る
サブアカウントも復活できる
- 🤖を押す
- \(\oplus\) Create or Restore account
- “Account 2” を Create Or Restore Account
Temple: 送金練習
↑: Account 1 から Account 2 にトークンを送ってみる
Tezos のスマートコントラクト
Michelson VM というスタックマシンを使用
Michelson は人間には辛いので高級言語を使う
- SmartPy https://smartpy.dev
- LIGO https://ligolang.org
- Archtype https://archetype-lang.org/
- SCaml https://scamlang.dev/, etc
Tezos のスマートコントラクト
- ストレージ
-
記憶領域。値を一つ取ります。
Map や Big_map を使うと巨大な連想配列データを保存できますが、
記憶域料金がかかります。これが Tezos での一番のコストになります。 - エントリポイント
-
複数のプログラム実行エントリポイントを持てます。
外部から機能の呼び分けが可能です。 - スマートコントラクトの動作
-
エントリポイントは送金命令からパラメータを受け取り、
そのコードを実行し、- ストレージの値の更新
- 必要であれば、さらなる送金命令の発行
を行います
SmartPy
Python で Tezos スマートコントラクトを書くと、
Michelson
にコンパイルしてくれる。
私は Python 書いたことありません
SmartPy: Browser IDE
ここでスマートコントラクトを:
- 書き
- テストし
- ブロックチェーンにデプロイできる
SmartPy IDE の使い方
SmartPy IDE の様子
- 左側にコードを書く
- 右側にコンパイル結果などが表示される
簡単な SmartPy コード
import smartpy as sp
class MyContract(sp.Contract):
def __init__(self, initx):
self.init(x = initx)
@sp.entry_point
def add(self, addx):
self.data.x += addx
@sp.add_test(name = "MyTest")
def test():
scenario = sp.test_scenario()
contract = MyContract(1)
scenario += contract
scenario += contract.add(2)
このコードを IDE にコピーペーストしてください
SmartPy のコンパイル
▶︎ を押すと Python コードが Michelson にコンパイルされます
ここでエラーがでたら直して再挑戦
コンパイルされた Michelson コード
Deploy Michelson Contract: コンパイル結果を見る
- Code: コード
-
parameter (int %add); storage int; code { UNPAIR; # @parameter : @storage # == add == # self.data.x += params # @parameter : @storage ADD; # int NIL operation; # list operation : int PAIR; # pair (list operation) int };
- Storage: ストレージ
-
1
スマートコントラクトのデプロイ
コンパイルされたコードをブロックチェーンへアップロード
Deploy Michelson Contract > Deploy Michelson Contract
デプロイ画面
- Ghostnet を選ぶ
- SELECT ACCOUNT
- Temple を選択
- パスワードを聞かれるかも
- Account 1 を選ぶ
- VALIDATE
- 必ず ESTIMATE COST FROM RPC を押す
これを忘れるとデプロイに成功したようで、しません。 - DEPLOY CONTRACT
- ACCEPT
- Temple が確認してくるので Sign する
デプロイの成功
ちゃんとデプロイが成功すれば、ブロックチェーンの進捗に従って Block Confirmations が育つはず
- CONTRACT ORIGINATED SUCCESSFULLY! と出たのに confirmation が育たない場合
- ESTIMATE COST FROM RPC の押し忘れ。やり直してください
デプロイされた様子を見る
- OPEN EXPLORER
現在のストレージの状態 (1
) が見れます
スマートコントラクトを呼び出してみる
- INTERACT
- スマートコントラクトに送るパラメーターを設定
- エントリポイント名 (
Add
) - パラメータ (例えば
42
)
スマートコントラクトを呼び出してみる
- Build Transaction Parameters
- SELECT ACCOUNT
- Temple を選んでアカウントを選ぶ
- Amount in tez: 同時に送るトークン量を決める: 今回は 0 tez で構わない
- Fee, Gas, Storage: いじらなくてOk
- CALL CONTRACT
✅が出て Block Confirmation が進めば成功
呼び出し結果を見る
Call result
実行の結果、ストレージが 1 + 42 = 43 になっている:
...
"operation_result": {
"status": "applied",
"storage": {
"int": "43"
},
...
SmartPy プログラムの書き方
簡単な SmartPy コード: import
sp
という名前でライブラリを使用:
必ず頭に書いてください。おまじないだと思っていいです。
他のライブラリの
import
はできません
Python の文法が使える DSL (ドメイン特化言語) であって、
Python
そのものではない
SmartPy: クラス
コントラクトは sp.Contract
のサブクラスとして定義:
これもおまじないだと思って構いません
SmartPy: ストレージ初期化
__init__
メソッドでデプロイ時のストレージの初期化を記述
デプロイ時に initx
を受け取り、ストレージ変数
x
を initx
にセットする
メソッド定義の第一引数は常に self
です。ここからクラス内部のメソッドやデータにアクセスできる
SmartPy: ストレージ初期化
一般的には:
__init__
では self.init(..)
を呼び出してストレージ初期化する以外のことはできません
Python なので字下げに注意
Python のプログラムの意味はインデントに影響されます。
正しいインデントを!
Error: expected an indented block
SmartPy: エントリポイント
エントリポイントは @sp.entry_point
アトリビュート付きのメソッドとして書く:
self.data.x += addx
は左辺の変数への加算。
self.data.x = self.data.x + addx
でもいいよ
SmartPy: エントリポイント
一般的にはエントリポイントの宣言は:
デフォルトエントリポイント名は
default
。特に指定せずアカウントに送金するとこのエントリポイントが呼ばれる
- ❗️️️
-
@sp.entry_point
を忘れがち。忘れると、
Error: New command outside of contract
などと言われる
SmartPy: ストレージ変数へのアクセス
self.data.ストレージ変数名
でアクセスする:
SmartPy: ストレージ変数へのアクセス
一般的にはストレージ変数へはアクセスは:
- ❗️️️️
-
.data
を忘れがち。忘れると、
unused parameter
エラーが出る。
SmartPy IDE: テスト
テストを記述すると、
- IDE からデプロイが可能になる
- 動作をシミュレートできる
SmartPy IDE: テスト結果
▶︎ を押すとコードをチェック、問題がなければテストを行う。
右側にテストの実行結果が表示される。
- デプロイされると初期ストレージは
x = 1
add
エントリポイントを2
のパラメータで呼び出すとx = 3
になる
ことがわかる
SmartPy IDE: デプロイ
Deploy Michelson Contract > Deploy Michelson Contract:
- 正しいネットワークを選ぶ (Ghostnet)
- 間違って Mainnet 選ぶとお金を取られる
- Temple を選んで使用するアカウントを選ぶ
- 事前に “REVEAL ACCOUNT” が必要な場合があります
- ❗️ “ESTIMATE COST FROM RPC” で正しいコストを見積もる
しないと失敗するようです - DEPLOY CONTRACT
- Block Confirmations を待つ
- OPEN EXPLORER で確認
- SAVE CONTRACT でアドレスを IDE に保存
SmartPy IDE: コントラクトの呼び出し
OPEN EXPLORER > INTERACT でエントリポイントを呼び出しできる
- エントリポイントと、呼び出しの引数を設定できる
- 使用感はデプロイと同じ
演習: 簡単な計算機を作る
add
以外に sub
, mul
,
div
エントリポイントを作って、
それぞれで、現在のストレージの値に対し減算、乗算、除算を行う
除算は整数。(SmartPy では /
と書いても //
になる。)
0除算
テスト時に 0除算があると、IDE ではエラーを検出してくれる。
ブロックチェーンにデプロイし、0除算を行う送金をすると?
→
送金命令として不正なのでブロックに取り込まれない
Failsafe な設計
Tezos では何かエラーが起これば送金全体が失敗し、なかったことになる。
Failsafety
Tezos では何かエラーが起これば送金全体が失敗する
もし fail_if_0(y)
を呼び出すと 0除算で失敗するが、
ストレージ x
は 42
にはならず、元の値のまま。
他のブロックチェーンでは、実行失敗時に、それまでの変更がストレージに残ってしまうものがある:
- どこで失敗するかの把握は難しい (例えば 0除算)
- 気付かぬうちに不変状態が破れた状態になると誤動作、脆弱性の元
演習: 複数のストレージ変数
2つのストレージ変数を持つ投票コントラクトを作ろう
x
とy
の二つのストレージ変数。初期値はそれぞれ0
vote_x
とvote_y
の二つのエントリ- 各々、
x
とy
に 1 足す
- 各々、
Python に ++
はありません。(私は、知らなかった💦)
まあ、認証もないし、投票と呼ぶのはおこがましい
演習: 複数引数の __init__
とエントリポイント
簡単だけど、
x
とy
の二つのストレージ変数- デプロイ時にそれぞれの値を指定できる
set_x_and_y
エントリ- 2つの整数を受け取り、
x
とy
をその値に変更
- 2つの整数を受け取り、
テスト呼び出しに注意
引数名の指定が必要:
contract.update(4, 3)
は怒られる
SmartPy: 関数定義
繰り返す操作は関数として定義しよう。
@sp.entry_point
のないメソッド定義
呼び出し
❗️ self.
をすぐ忘れる
SmartPy: 条件分岐
https://smartpy.dev/docs/general/control_statements
SmartPy のふしぎポイント。
Python の if
文は使えない。 sp.if
メソッドを使う:
# 偶奇で分岐
sp.if x % 2 = 0: # % は剰余
self.data.result += 1
sp.else: # if, else の後の : を忘れがち
self.data.result += -1
❗️ sp.if
と sp.else
の sp.
は書くのをとかく忘れがち
SmartPy: エラー終了
https://smartpy.dev/docs/general/raising_exceptions
コントラクト実行をエラーで終了したい時は
sp.failwith("メッセージ")
sp.failwith
するとコントラクト実行は失敗する:
- 実行を引き起こしたトランザクション自体も失敗する
- 全ての状態はトランザクション前のまま。元に戻す心配はしなくて良い
SmartPy: 基礎データ型
(ざっくりいきます。必要になったら見返してください)
SmartPy: 基礎データ型
https://smartpy.dev/docs/general/types#basic-types
int
ornat
-
1
,2
,3
string
-
"hello"
bytes
-
sp.bytes("0x1234")
# Hex で表記 bool
-
True
,False
unit
-
Unit
しか値を取らない型
SmartPy: int
と nat
https://smartpy.dev/docs/general/types#nat
ほとんど型が出てこないが SmartPy は型付きの言語。(Michelson も型付きなので)
\(\mathbb{Z}\)の型 int
と \(\mathbb{N}\) の型 nat
がある
ほとんどの場合、自動判別されるが明示することも可能:
sp.int(-3)
,sp.nat(42)
。sp.nat(-10)
はエラー- 変数に対しては
sp.set_Type(変数, 型)
で宣言:
int
と nat
は別の型
https://smartpy.dev/docs/general/types#nat
sp.int(10) + sp.nat(20) # エラー
明示的に型変換する必要がある:
sp.int(10) + sp.to_int(sp.nat(20))
int
から nat
は失敗する可能性がある:
sp.as_nat(int(10))
nat - nat
は nat
ではなく int
!!!
sp.nat(12) - sp.nat(10) == sp.int(2)
sp.nat(10) - sp.nat(12) == sp.int(-2)
SmartPy: 文字列 string
x = "Hello World"
# 連結
y = "Hello" + "World"
# 文字列リストの連結
sp.concat([ "Hello", " ", "World" ])
# 長さ。nat です
sp.len("Hello") == sp.nat(5)
# サブストリング。成功すると Some サブストリング を返す
sp.slice("Hello", 1, 3) == sp.some("ell")
# サブストリング。範囲外になると None を返す
sp.slice("Hello", 3, 7) == sp.none
ASCII だけ。日本語は使えません。 日本語はオフチェーンで扱う
SmartPy: バイトデータ bytes
x = sp.bytes("0x2345")
# 連結
x = sp.bytes("0x1234") + sp.bytes("0x5678")
# バイトリストの連結
x = sp.concat([ sp.bytes("0x12"), sp.bytes("0x34"),
sp.bytes("0x56") ])
# 長さ。結果は nat です
sp.if sp.len(sp.bytes("0x1234")) == 2:
# サブバイト
sp.slice(sp.bytes("0x12345678"), 1, 2)
== sp.some( sp.bytes("0x3456"))
sp.if sp.slice(sp.bytes("0x12345678"), 3, 7) == sp.none
SmartPy: Booleans
True
,False
~True == False
(True | False) == True
(True & False) == False
SmartPy: コンテナ型
(きりがないです。必要になったら見返してください)
SmartPy: タプル
https://smartpy.dev/docs/types/pairs
Tuple でも Pair と呼ぶ
Tuple の分解
SmartPy: リスト
https://smartpy.dev/docs/types/lists
Array では…ありません。
リストの分解
SmartPy: option
https://smartpy.dev/docs/types/options
あるか、ないか。
Option の分解
SmartPy: 連想配列: Map と Big_map
https://smartpy.dev/docs/types/maps_and_big_maps
Map は:
Big_map は作成時に sp.big_map(...)
を明示します:
違い
- Map は早いですが巨大な連想配列には向きません
- Big_map は遅いですがロード費用が安いのでデータベース用の連想配列です
SmartPy: 連想配列: Map と Big_map
https://smartpy.dev/docs/types/maps_and_big_maps
初期化
読み出し
更新
削除
SmartPy: レコード
https://smartpy.dev/docs/types/records
レコードの生成
フィールドアクセス
SmartPy: バリアント
https://smartpy.dev/docs/types/variants
タグ付きユニオンのようなもの
SmartPy: バリアント #2
https://smartpy.dev/docs/types/variants
バリアントの分解
もうちょっとブロックチェーンぽいもの
SmartPy: グローバルプロパティ
https://smartpy.dev/docs/types/mutez#global-properties
コントラクトが実行された際の様々な値を sp.変数名
で
アクセスできる:
sp.amount
: 送金額sp.balance
: コントラクトが持つ残高sp.source
: トランザクションを引き起こしたアカウントsp.sender
: コントラクトを直接呼び出したアカウント- etc
SmartPy: アドレス
演習: 認証をしてみる
送金命令を出した人(sp.source
)が設定した人でなければ
カウンタを増やせない
テスト時に source
を指定
エントリポイント呼び出しの後に
.run(source = アドレス)
SmartPy: トークン量(tez)
sp.tez(1)
: 1 tezsp.mutez(1)
: 0.000001 tez
mutez (= μtez, \(10^{-6}\) tez) が最小単位。整数として取り扱う。
SmartPy: コントラクトからの送金
https://smartpy.dev/docs/types/contracts_and_addresses#transfer
- 送金
-
sp.send(送金先, トークン量)
- スマートコントラクト呼び出し
-
sp.transfer(引数, トークン量, 送金先)
例: コントラクトの残高を全て送金する:
演習: ブーメラン
送金されたら、同じ額を source に返す
演習: クラウドファンディング(ブラックホール)
呼び出した人とその総送金額を big_map に記録します。
初期化
送金があったとき
演習: クラウドファンディング #2
先ほどのクラウドファンディング(ブラックホール)、このままではせっかく寄付してくれたトークンを引き出すことができません!
認証を施して、自分がアクセスしたときだけ、溜まったトークンを自分に全額送金するようにしてください。
演習: 銀行
先ほどのクラウドファンディング(ブラックホール)を改造して
銀行を作れます。
withdraw
エントリポイントを作って、各ユーザーが指定した額を引き出せるようにしてください。
- source の残高以上の額を引き出せてはいけない
- 引き出したら残高を更新しなければならない
演習: 俺俺コイン
クラウドファンディングや銀行では tez を扱いましたが、 tez の代わりに
nat
を扱うようにすると fungible token (のようなもの)
が作れます。
mint(amount)
-
amount
量の俺コインを呼び出した人に与えます。管理者しか実行できません transfer(dest, amount)
-
amount
量の俺コインを source の残高から引き、それをdest
で指定したアドレスの残高に足します。
SmartPy: この機能がないと思ったら
リストのn番目が欲しい…
ありません!
Tezos の Michelson VM はスマートコントラクトの安全性のために array などのヤバい機能は入っていません。
SmartPy も普通の Python だと思っていると、いろんなものがありません。あまり、無いものを探し回らないように。
何か探すときは https://smartpy.dev/docs/manual/introduction/overview の左側からか 🔍Search を使ってください。
オフチェーンプログラミング
本来は、オンチェーンのスマートコントラクトを作るだけでなく、 そこにアクセスしやすくデザインしたオフチェーンのソフトウェアも必要です:
- ウォレット連携
- ブロックチェーンからの情報取得
- 送信するデータ生成
- GUI
オフチェーンプログラムに比べると、スマートコントラクトは短めです。
ただし、金銭を扱う公開ソフトウェアとなるので、
バグがあると大変です。
(次回話します)
課題
次のいずれかを SmartPy で実装し、Ghostnet にデプロイしてください:
- クラウドファンディング#2 (1点)
- 銀行 (2点)
- 俺俺コイン (3点)
- 上の三つより何か面白いもの (1~6点)
提出物:
- 何を実装したか。4 の場合は何を作ったのか説明をください
- デプロイした
KT1*
アドレス