見出し画像

【Sui】スマート・コントラクト開発 - MoveとRustの比較(Move言語とその安全性)

この記事は、 Krešimir Klas さんが執筆した "Smart Contract Development — Move vs. Rust" (2022年9月7日公開) という記事を、本人の許可をいただいた上で翻訳・補足したものです。

1章~3章はこちら


4. Moveの安全性

ここまでMoveについて以下のことがわかった

  • 自分が所有・共有するオブジェクトを、どのモジュール内のどの関数にも渡すことができる

  • 誰でもモジュールを公開できる

  • Solanaのアカウントのように、モジュールが構造体を所有することで、所有モジュールが構造体の変異に対して唯一の権限を持つという概念はない。

  • Solanaのアカウントとは異なり、「モジュールが構造体を所有し、所有するモジュールだけがそれを変更する権限を持つ」という概念がない。構造体は他のモジュールに流れ込み、他の構造体に埋め込まれることもある。

問題は、何がこれを安全にしているのかということだ。誰かが敵対的なモジュールを公開し、(AMMプールのような)共有オブジェクトをその敵対的なモジュールに送り込み、それを使って資金を奪い取ることを阻止できるのだろうか?

Solanaでは、アカウントの所有権という概念があり、アカウントを所有するプログラムだけがそのアカウントを変更させることができる。しかしMoveでは、モジュールがオブジェクトを所有するという概念はなく、オブジェクトを任意のモジュールに送ることができる—オブジェクトの参照だけでなく、値そのものを送ることができる。だが、このオブジェクトが信頼されていないモジュールを通過している間に不正に変更されていないことをチェックする機構はランタイムには存在しない。では、何がこのオブジェクトの安全を保っているのだろうか?オブジェクトが信頼されていないコードによって悪用されないという保証はどこから来るのだろうか?

💡 Moveの「すべてがオブジェクト」という概念が際立つ説明ですね

さて、ここにMoveの新しさがある。

4.1. 構造体(Structs)

構造体タイプの定義は、皆さんが期待する通りのものである:

struct Foo {
  x: u64、
  y: bool
}

💡 Move 2024では、struct には以下のように public visibility modifier が必須となっています (ドキュメント)
```
public struct Foo {}
```

これはRustで構造体を定義する方法と同じである。しかし、Moveの構造体にはユニークな点がある。Moveでは、従来のプログラミング言語よりも、モジュールが型の使用できるか・できないかをより詳細に制御できるという点である。上のコードスニペットのように定義された構造体には、次のような制限がある:

  • 構造体のインスタンス化(パック)と破棄(アンパック)は、構造体を定義するモジュール内でのみ可能である。つまり、他のモジュール内の関数から構造体をインスタンス化または破棄することはできない

  • 構造体インスタンスのフィールドは、そのモジュールからのみアクセス(および変更)できる。

  • 構造体インスタンスのクローンや複製は、そのモジュールの外部ではできない

  • 構造体インスタンスを他の構造体インスタンスのフィールドに格納することはできない

つまり、他のモジュール内の関数でこの構造体のインスタンスを扱う場合、そのフィールドを変更させたり、クローンしたり、別の構造体のフィールドに格納したり、破棄したりすることはできない(関数呼び出しによって別の場所に渡す必要がある)。構造体を定義したモジュールがこれらの操作を行う関数を実装しており、それを私たちが作成した別のモジュールから呼び出すことはできるかもしれないが、そうでない場合は、外部の型に対して直接操作をすることはできない。これにより、モジュールは自分の型をどのように使用できるか・できないかを完全に制御できるようになっている。

さて、このような制限によって、多くの柔軟性を失ったように思える。その通りだ。このような構造体を扱うことは、従来のプログラミングでは非常に面倒なことだが、実はこれこそがスマートコントラクトに求められていることなのだ。スマートコントラクトの開発とは、結局のところ、デジタル資産(リソース)をプログラミングすることなのだ。上述の構造体を見てみると、これはまさにリソースだ。何もないところから恣意的に作り出すことも、複製することも、間違って破壊することもできない。確かにここではいくるかの柔軟性失ったが、失われた柔軟性こそ、私たちが失いたい柔軟性なのである。これにより、リソースを扱うことが直感的かつ安全になる。

💡 太線、テストにでます!!

さらに、Moveでは構造体に能力を追加することで、これらの制限をある程度緩めることができる。キー(key)、保存(store)、コピー(copy)、破棄(drop)の4つの機能がある。構造体には、これらの機能を自由に組み合わせて追加できる:

struct Foo has key, store, copy, drop {
  id: UID,
  x: u64,
  y: bool
}
  •  key - 構造体がオブジェクトにすることができる(Sui固有のもので、コアのMoveは若干異なる)。前述したように、オブジェクトは永続化され、所有オブジェクトの場合は、スマートコントラクトの呼び出しで使用するためにユーザー署名が必要になる。keyを使用する場合、構造体の最初のフィールドはUID型のオブジェクトのIDでなければならない。これにより、、グローバルに一意のIDが与えられ、参照に使用できる

  • store - こ構造体を別の構造体のフィールドとして埋め込むことができる

  • copy - 構造体を任意の場所からコピー/クローン化できる

  • drop - 構造体を任意の場所から破棄できる

💡 key がオブジェクトそのものということですね

要するに、Moveのすべての構造体はデフォルトでリソースである。これらの能力を使用すると、制限を細かく緩和して、従来の構造体のように振る舞わせるための力を与えてくれる

4.2. コイン(Coin)

具体例としてCoin型を見てみよう。CoinはSuiでERC20 / SPLトークンのような機能を実装しており、Sui Moveライブラリの一部だ。以下のように定義されている:

// coin.move
struct Coin<phantom T> has key, store {
    id: UID,
    balance: Balance<T>
}

// balance.move
struct Balance<phantom T> has store {
    value: u64
}

モジュールの完全な実装はSuiコードベースにある

Coin型はkeystore機能を持っている。keyはオブジェクトとして使えることを意味する。これにより、ユーザーはCoinを(トップレベルのオブジェクトとして)直接所有することができる。Coinを所有している場合、所有者以外はトランザクションでそれを参照することすらできない(使用することもできない)。storeとは、Coinを別の構造体のフィールドとして埋め込むことができることを意味する。これはコンポーザビリティに役立つ。

drop機能がないため、関数内でコインを誤って破棄(消去)することはできない。これはとても便利な機能で、誤ってCoinを失うことがない。Coinを引数として受け取る関数を実装している場合、関数の最後には明示的にCoinで何かをする必要がある。例えば、Coinをユーザーに転送する、別のオブジェクトに埋め込む、呼び出しによって別の関数に送る(この関数でもCoinで何かをする必要あり)などである。もちろん、coinモジュールのcoin::burn関数を呼び出してCoinを破棄することは可能だが、それは意図的に行う必要がある(誤って行うことはない)。

clone機能がないため、誰もコインを複製することができず、無から新しい通貨を生み出すことができない。新しい通貨を作るにはcoin::mint関数を使うが、この関数はコインのtreasury capabilityオブジェクト(このオブジェクトは最初に通貨作成者に譲渡される)の所有者のみが呼び出すことができる。

また、ジェネリックスのおかげで、異なるコインはそれぞれ別の型になる。また、2つのコインはcoin::join関数を通じてのみであり、そのフィールドに直接アクセスして足し合わせることはできない。つまり、異なる型のコイン(コインA+コインB)の値を足し合わせることはできない。型システムは、不適切な会計処理を防いでくれる。

Moveでは、リソースの安全性はその型によって定義される。Moveがグローバルな型システムを持っていることを考えると、これによって、より自然で安全なプログラミング・モデルが可能になり、リソースの安全性を維持したまま、信頼されていないコードに直接リソースを受け渡しできるようになる。これは一見大したことではなさそうだが、実はスマートコントラクトのコンポーザビリティ、使い勝手、安全性にとって大きなメリットがある。これについては第5章で詳しく説明する。

4.3. バイトコードの検証

前述したように、Moveスマートコントラクトはモジュールとして公開される。そして、誰でも任意のモジュールを作成し、ブロックチェーンにアップロードして、誰でも実行できるようになっている。また、Moveには構造体の使用方法について一定のルールがあることも見てきた。

では、これらのルールが任意のモジュールによって遵守されることを保証するものは何だろうか?例えば、特別に作成されたバイトコードを含むモジュールをアップロードし、コインオブジェクトを受け取ってその内部フィールドを直接変更することで、これらのルールを回避することを防ぐことができるのだろうか?これができてしまうと、コインの量を不正に増やすことができる。バイトコードの構文自体はこれを可能にする。

このような悪用を防ぐのが、バイトコードの検証である。Moveのバイトコード検証ツールは、Moveバイトコードを静的に分析し、必要な型、安全性、およびリソース安全性のルールを守っているかどうかを判断する。チェーンにアップロードされるすべてのコードは、この検証を通過する必要がある。Moveモジュールをチェーンにアップロードしようとすると、ノードとバリデータはまずそれを検証ツールにかけ、コミットする前に確認する。Moveの安全ルールを回避しようとするモジュールがあれば、それは検証ツールによって拒否され、公開されない。そのため、細工されたバイトコードで型やリソースの安全ルールを破ることはできない。検証がそのようなモジュールのチェーンへのアップロードを防ぐからだ!

Moveバイトコードと検証機能は、Moveの核となる機能である。これは、他の方法では不可能な、リソースを中心とした直感的なプログラミング・モデルを可能にするものだ。重要なのは、構造化された型を、その完全性を失うことなく信頼の境界を越えて渡すことができるということだ。


Solanaではスマートコントラクトはプログラムであり、Moveではモジュールである。これは単なる言葉の違いのように思えるかもしれないが、そうではなく、非常に大きな意味を持つ。

違いは、Solanaではプログラムの境界を越えた型安全性がないという点である。各プログラムは生のアカウントデータからインスタンスを手動でデコードしてロードし、これには重要な安全チェックを手動で行うことが含まれる。また、ネイティブなリソース安全性もない。代わりに、各スマートコントラクトが個別にリソース安全性を実装する必要がある。これにより十分なプログラミング可能性は確保されるが、Moveのモデルと比べると、コンポーザビリティと使い勝手が大幅に制限される。Moveでは、リソースのネイティブサポートがあり、信頼できないコードに安全に出入りさせることができる。

Moveでは、型はモジュール間にまたがって存在し、型システムはグローバルである。つまり、CPIの呼び出し、アカウントのエンコード/デコード、アカウント所有権チェックなどは必要なく、引数を使って別のモジュールの関数を直接呼び出すだけでよい

💡 Sui の DevX(開発者体験)が優れている理由はここにありそうでうね

スマートコントラクト全体の型とリソースの安全性は、コンパイル/公開時のバイトコード検証によって保証され、Solanaのようにスマートコントラクトレベルで実装して実行時にチェックする必要はない。


4章終わり

この記事が気に入ったらサポートをしてみませんか?