この記事は筆者のソロ Advent Calendar 2022 21日目の記事です。
引き続きflowブロックチェーン上にスマートコントラクトを実装するためのCadenceという言語について公式ドキュメントのチュートリアルをやってみたのでその備忘録です。
今回は前回やったリソースへのアクセスを拡張する方法について紹介していきたいと思います。
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Hello World編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[リソース編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[capability リンクの参照とスクリプト] <- 今ここ
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編1]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編2]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Fungible Tokens(FT)編]
チュートリアルplaygroundはこちら
デプロイするコントラクトは前回同様こちら
pub contract HelloWorld {
// Declare a resource that only includes one function.
pub resource HelloAsset {
// A transaction can call this function to get the "Hello, World!"
// message from the resource.
pub fun hello(): String {
return "Hello, World!"
}
}
// We're going to use the built-in create function to create a new instance
// of the HelloAsset resource
pub fun createHelloAsset(): @HelloAsset {
return <-create HelloAsset()
}
init() {
log("Hello Asset")
}
}
capability
日本語で何て言えばいいのかわからないのでそのまま使いますがリソースへのアクセスを拡張するのにcapabilityを使用することができます。なぜ、リソースへのアクセスを拡張する必要があるかは公式ドキュメントの例がわかりやすいのでそのまま引用させていただきます。(原文を翻訳しています)
実際のユーザーのアカウントには、さまざまなレベルのアクセス範囲とプライバシーが必要な関数やフィールドが含まれます。例えば、ユーザーがトークンを交換できるアプリを開発しているとします。アカウントからトークンを引き出すような機能には書き込み権限を署名する必要がありますが、アプリでは誰でもトークンを預けることができるようにする必要があります。ユーザーがアプリを初めて認証した後、アプリでトークンを引き出せるようにする機能を作れば、アカウントのトークンを引き出して使ったり取引したりできるトランザクションを書くことができ、より便利になります。
チュートリアルplaygroundにCreate Linkトランザクションが用意されており、中身は以下のような内容になります。
import HelloWorld from 0x01
// This transaction creates a new capability
// for the HelloAsset resource in storage
// and adds it to the account's public area.
//
// Other accounts and scripts can use this capability
// to create a reference to the private object to be able to
// access its fields and call its methods.
transaction {
prepare(account: AuthAccount) {
// Create a public capability by linking the capability to
// a `target` object in account storage.
// The capability allows access to the object through an
// interface defined by the owner.
// This does not check if the link is valid or if the target exists.
// It just creates the capability.
// The capability is created and stored at /public/Hello, and is
// also returned from the function.
let capability = account.link<&HelloWorld.HelloAsset>(/public/HelloAssetTutorial, target: /storage/HelloAssetTutorial)
// Use the capability's borrow method to create a new reference
// to the object that the capability links to
// We use optional chaining "??" to get the value because
// result of the borrow could fail, so it is an optional.
// If the optional is nil,
// the panic will happen with a descriptive error message
let helloReference = capability.borrow()
?? panic("Could not borrow a reference to the hello capability")
// Call the hello function using the reference
// to the HelloAsset resource.
//
log(helloReference.hello())
}
}
前回までと同様HelloWorldコントラクトを0x01アカウントからインポートしていてトランザクションブロックの中のprepareブロックでcapabilityを作成し、使用する処理が書かれています。一行ずつ内容を見ていきます。
capabilityの作成
let capability = account.link<&HelloWorld.HelloAsset>(/public/HelloAssetTutorial, target: /storage/HelloAssetTutorial)
アカウントストレージAPIのlinkメソッドを使用することでそのアカウントへのリンクにアクセスできるcapabilityを作成することができます。HelloAssetリソースはアカウントストレージ内の/storage/HelloAssetTutorial
に保存されており、アカウント所有者だけがアクセスできます。このリソースのhello関数を誰でも呼べるようにするために/public/HelloAssetTutorial
というパスにcapabilityを作成します。
型パラメーターで指定しているのはアクセスできるようにするオブジェクトの参照となっており、参照は型の先頭に&
をつけることで表します。この参照は他の言語で言うポインタのようなものと考えて大丈夫なようです。
第二引数のtargetパラメーターに指定するのはリンク先のストレージ内のオブジェクトのパスを指定します。
capabilityを格納するパスは少数または一人に公開するのであればprivate
を使い、全体公開であればpublic
とパスの記述を使い分けることができます。
参照の作成
参照を作成するコードは以下の行です。
let helloReference = capability.borrow()
?? panic("Could not borrow a reference to the hello capability")
参照を作成するにはcapabilityのborrowメソッドを使用することで作成することができます。borrow関数で作成する参照を借用している間は参照の借用に失敗する可能性があるため??
を使用し、borrow関数の実行結果がnil
だった場合はpanicを起こしてエラーメッセージを出力しています。
このように、処理を機能と参照に分離することでリエントランシー攻撃から保護することができます。リエントランシー攻撃とは、悪意のあるユーザーがオブジェクトを複数回呼び出すことができると言うもので、このような攻撃は他のスマートコントラクト言語を悩ませてきたようです。オブジェクトへの参照は一度に1つしか存在できないのでCadenceを使用する場合、ストレージ内のオブジェクトに対してこの種の脆弱性はありえません。
また、オブジェクトのオーナーは参照するオブジェクトを移動するか、unlink
関数でリンクを破壊することが可能で、その場合capabilityは無効となります。
参照オブジェクトの実行
以下のようにして借用した参照から関数を実行できます。
log(helloReference.hello())
実行結果
Create Hello "Saved Hello Resource to account."
Create Link "Hello, World!"
scriptの実行
scriptはシンプルなトランザクションタイプで、書き込みは出来ず読み込みのみが可能なトランザクションを実行します。scriptはどのアカウントからも許可されずに実行されるため、scriptにはアカウントのストレージにアクセスするcapabilityが必要です。scriptを実行するにはpub fun main()
という関数を作成します。
playgroundのScript TemplatesにHelloWorldコントラクトの参照を借用してhello関数を実行するスクリプトがあります。中身は以下のようになっています。
import HelloWorld from 0x01
// A script is a special type of Cadence transaction
// that does not have access to any account's storage
// and cannot modify state. Any state changes that it would
// do are reverted after execution.
//
// Scripts must have the following signature: pub fun main()import HelloWorld from 0x02
pub fun main() {
// Cadence code can get an account's public account object
// by using the getAccount() built-in function.
let helloAccount = getAccount(0x01)
// Get the public capability from the public path of the owner's account
let helloCapability = helloAccount.getCapability<&HelloWorld.HelloAsset>(/public/HelloAssetTutorial)
// borrow a reference for the capability
let helloReference = helloCapability.borrow()
?? panic("Could not borrow a reference to the hello capability")
// The log built-in function logs its argument to stdout.
log(helloReference.hello())
}
ビルトイン関数であるgetAccount()
を使用し、PublicAccount
オブジェクトを取得します。そして、アカウントオブジェクトのgetCapability()
を使用し、参照を取得したら、あとは同じような流れで参照を借用し、関数を実行します。getCapability
関数の引数にはcapabilityが格納されているパスと型パラメーターには対象オブジェクトの型参照を指定します。
このscriptを実行しても前回と同じような結果が得られることがわかります。
Script 1 "Hello, World!"
Script 1 Result {"type":"Void"}
まとめ
今回は以下のことについて紹介しました。
- リソースへのアクセスを拡張するcapabilityという機能について
- capabilityから対象オブジェクトの参照を取得し、実行する方法について
- scriptの実行について
Cadenceはドキュメントもしっかりしていますし、web2で使うような言語と文法が似ているためかなり読みやすいです!まだチュートリアルの内容を動かした程度ですが引き続き次回以降もcadenceについての記事を書いていきたいと思います!今回は以上です!