Cadenceのかんどころ(4)制限型・Capability型
前回
前回はインタプリタの仕組みを調査して、参照型の正体に迫った。今回は制限型というCadenceでインターフェイスを扱うときに使う考え方と、Cadenceで最も重要な概念の1つであるCapabilityについて紹介したいと思う。
Restricted Type)
制限型(Cadenceでインターフェイスを関数の引数に取ったり、変数の型をインターフェイス型に指定したい時に使われる。例えばJavaだと
interface Shape {
int getArea();
}
class Rectangle implements Shape {
…
}
Shape shape = new Rectangle (); // これをCadenceでやりたい!
と書くことをCadenceでやりたいときは
var shape: AnyStruct{Shape} = Rectangle()
のように{}
でインターフェイスの型を囲う。この{}
の記号が制限型を作っていて、AnyStruct{Shape}
は「Shapeインターフェイスを実装したAnyStruct型」という意味になる。T{U}
とすることで、T
型だがU
型に制限された型を表現することができる。任意のShapeインターフェイスを実装した型Tに対して、T{Shape}
という書き方ができる。T{Shape}型はShape型の機能に制限されたT型という意味になる。T型であって欲しいけど、Tの中でShape型の機能しか持たせたくないときに重宝される。これは読み書きができるオブジェクトの型をある場所ではReadonlyにしたり別の場所では書き込みを出来たりするといった権限管理の用途でよく使われる。
ちなみにインターフェイスの部分はT{U1, U2, U3}
という風に複数指定でき、リソース型ならAnyStruct
が@AnyResource
になる。またTがAnyStruct, @AnyResourceの場合は省略できる。
var shape: {Shape} = Rectangle() // AnyStructは省略できる
var c: @{HasCount} <- create Counter() // AnyResourceは省略できる
参照型も制限型にできる。
var shape: &{Shape} = &Rectangle()
var c <- create Counter()
var cr: &{HasCount} = &c as &{HasCount}
Capability)
ケーパビリティ型(ストレージに保管されているオブジェクトはその所有者アカウントからしかアクセスできない。しかしコントラクトが人のストレージの値を読み込んだり、書き込むことができないとアプリケーションは設計できない。コントラクトを介したトークンの移動や状態の変更が実装できなくなってしまうからだ。そこでCadenceではストレージ内のオブジェクトを直接触らせずに、そのオブジェクトに対する権限を外部から触れるようにすることで、間接的にオブジェクトとのやり取りを実現する。
例えば、ストレージ上のオブジェクトをファイルシステム上のファイルのようなものだとすると、Read権限やWrite権限を特定ユーザー(アカウント)に渡すような仕組みが考えられる。Flowではそのような権限を抽象化したCapability型のオブジェクトによって、ストレージ内のオブジェクトに関する操作権限が表現される。コード上では、Capability型は基本的に型引数Tを取るのでCapability<T>型で目にすることがほとんどである。
ストレージにあるT型オブジェクトのインスタンスt
を指すCapability<T>型オブジェクトc
があるとき、c
を使ってt
のメンバ関数を呼び出すことができる。T型を制限型にすれば、単なるRead, Writeにとどまらない多様で柔軟な権限が、Capability<T>型のオブジェクトとしてインスタンス化されるのである。Capabilityが権限だとすれば、その権限が及ぶ対象がt
で、c
を所有することはT型に定められた権限を所有しているいうことになる。
Capability<T>型インスタンス化された権限は、権限行使対象であるストレージ上のT型のオブジェクトをある意味で参照している。この関係を、あるCapability<T>インスタンスとストレージ内のT型オブジェクトの間に結びつき(link)があると呼んでも良い。
そんなCapability型にはコンストラクタがない。インスタンス化する時には、AuthAccount型のメンバ関数であるlink関数が代わりに使われる。link関数は以下のようなシグネチャをしている。
fun link<T: &Any>(_ newCapabilityPath: CapabilityPath, target: Path): Capability<T>?
いろいろなことが起きているが、一番分かりやすい部分であるのはtarget
だろう。target
には、作られるCapabilityと結びつきがある権限行使対象オブジェクトが保管されたストレージのパスが指定される。link関数がAuthAccount型のメンバ関数なのは、ストレージへの権限であるCapabilityが署名者(AuthAccount)以外に生成されるのを防ぐためだ。そのため、署名なしで呼べるPublicAccount型のアカウントにlink関数は生えていない。つまり他人のストレージのオブジェクトに対してlinkすることはできない。
他の部分を説明するために具体的な例を見てみよう。
// PlayerNFT contract
pub resource interface HasAge {
pub fun getAge(): Int
}
pub resource Player {
pub var age: Int
init() {
self.age = 10
}
pub fun increment() {
self.age = self.age + 1
}
pub fun getAge(): Int {
return self.age
}
}
...
// Transaction スクリプト
transaction {
prepare(acct: AuthAccount) {
let storagePath: StoragePath = /storage/PlayerNFTPath
let capPath: CapabilityPath = /public/HasAge
// このcapabilityはCapability<&AnyResource{PlayerNFT.HasAge}>型
let capability = acct.link<&{PlayerNFT.HasAge}>(capPath, target: storagePath)
?? panic("some error")
}
execute {
log("success")
}
}
このシリーズの第2回でストレージには2種類あると説明したことを思い出して欲しい。
Capabilityオブジェクトがlink関数でインスタンス化されると、同時に生成主のCapabilityストレージ保管される。その保管先がCapabilityPath型の引数であるnewCapabilityPath
で指定されるのだ。T
の部分は権限行使対象オブジェクトの型を指定する。今回はTのインターフェイスであるHasAgeインターフェイスを使った制限型AnyReaource{HasAge}
の参照型である&{HasAge}
が指定されている。(前述の通りAnyResource
は省略して書ける。)参照型でないといけないのは、link
関数のシグネチャでT: &Any
となっている通り、いわゆるジェネリクス制約によって決められているからだ。
普通のストレージ(StoragePath)には参照型は保管できない。参照型はStorableな型ではないからだ。そういう意味でも、Capabilityは参照型をCapabilityストレージという特殊な場所に保管する仕組みだと言える。
この権限行使対象のオブジェクトはHasAge
を実装したオブジェクトでなければならず、そうでないと型変換エラーが起こる。
前述の通りT
には制限型を含む任意の型が指定できるので、Read, Write権限以上に柔軟な権限をT
に表現できる。
返し値はOptional型だ。newCapabilityPath
にnewとあるように、既にこのパスにCapabilityがあるとnil
が返って来る。同様に、target
となる権限行使対象が存在しないときもnil
が返る。
Capabilityを行使する
link関数が取るnewCapabilityPath
という引数は、CapabilityPath型だった。このCapabilityPath型には二つの子クラスPublicPath型とPrivatePath型があったのを思い出せるだろうか。つまりCapabilityを保管する先にはpublicなパスとprivateなパスの二つから選べるのである。
Publicなパスには、PublicAccount型からアクセスでき、PrivateなパスにはAuthAccount型、つまり署名がないとアクセスできない。
これはCapabilityを取って来るgetCapability関数のシグネチャを見ても明らかだ。
PublicAccount型に生えたgetCapability関数
fun getCapability<T>(_ path: PublicPath): Capability<T>
AuthAccount型に生えたgetCapability関数
fun getCapability<T>(_ path: CapabilityPath): Capability<T>
PublicAccount型からはPublicPath型のパスしか指定できないのに対して、AuthAccount型からは両方指定できる。
Capability<T>は権限であり、Tは権限によって行使できる操作である。実際Capability<T>からTを取り出して行使するためにはborrowというメンバ関数を呼ぶ。
fun borrow<T: &Any>(): T?
この関数によってnil
でないT
が返ってきて初めてT
を使った操作が行えるのだ。実際の例を見てみる。
transaction {
prepare(acct: AuthAccount) {
let storagePath: StoragePath = /storage/PlayerNFTPath
let capPath: CapabilityPath = /public/HasAge
// このcapabilityはCapability<&AnyResource{PlayerNFT.HasAge}>型
let capability = acct.getCapability<&{PlayerNFT.HasAge}>(capPath)
// このhasAgeは&AnyResource{PlayerNFT.HasAge}型
let hasAge = capability.borrow() ?? panic("Could not borrow")
// 権限が与えられた操作の実行
log(hasAge.getAge())
}
execute {
log("success")
}
}
borrow<T>()
のT
が省略されると、直前のgetCapacity<T>
から型推論される。逆に以下のようにTの指定をborrow時に行っても良い。
let capability = acct.getCapability(capPath)
let hasAge = capability.borrow<&{PlayerNFT.HasAge}>() ?? panic("Could not borrow")
CapabilityはStorableなオブジェクト
Capability型がCapabilityパスに保管できることはつまり、Storableであることを意味する。これはインタプリタ内でのCapability型の内部表現を見ても分かる。
Capability型のオブジェクトは必ずしもCapabilityPathに保管されるとは限らないのだ。つまりどこにでも保管できる。また、何かのクラスにwrapさせても良い。リソース型のクラスのメンバとしてCapabilityを持たせれば、Capabilityをリソースとして扱うこともできる。
// CapabilityオブジェクトをCapabilityパスから読み込む
let capability = acct.getCapability<&{PlayerNFT.HasAge}>(/public/HasAge)
// Capabilityオブジェクトを普通のストレージに保存する
acct.save<Capability<&AnyResource{PlayerNFT.HasAge}>>(capability, to: /storage/MyCapability)
// Capabilityオブジェクトを普通のストレージから読み込む
let c = acct.load<Capability<&AnyResource{PlayerNFT.HasAge}>>(from: /storage/MyCapability) ?? panic("capability not loaded")
Cadenceで自分以外のアカウント(PublicAccount)のストレージ状態を変更するために必ずCapabilityが必要となることを思い出して欲しい。これを上手く使って、コード内でのCapabilityの流れが見えるようにすれば、どこでストレージの状態が変更されうるか可視化されて安全なコントラクトを記述することができる。
このような考え方はCapability-based securityと呼ばれていて、Cadenceはこの考え方を上手くスマートコントラクトの世界に導入したと言える。
おわりに
長々としたシリーズになってしまったが、Cadenceの世界に興味を持っていただけると幸いである。また、間違いなどあれば指摘していただけるとありがたい。
Discussion