基本的なスマートコントラクトをRustで書く方法

スマートコントラクトとは

スマートコントラクトは、ブロックチェーン上にインストールされた自動実行するプログラムです。Casper Networkのコンテキストでは、スマートコントラクトはトランザクションにてチェーン上にインストールされたコントラクトコードから成り立っています。Casperのスマートコントラクトは、Casperネットワーク上で実行されるために作られています。色々なトリガーやコンディション、そしてロジックを扱いながらエントリーポイントを介してアカウントや他のコントラクトとやり取りします。

スマートコントラクトは、チェーン上に格納されたロジックとして存在し、どのユーザーも、エントリーポイントを呼び出すことができます。また、コントラクトが他のコントラクトを呼び出し、内部連携したオペレーションの実行やより複雑なプログラムを作成することもできます。ブロックチェーン技術の分散型特性は、スマートコントラクトがいかなる失敗一つにも影響されないことを意味しています。Casperノードの内1つがネットワークから離脱したとしても、他のノードが問題なくコントラクトを継続して運用していきます。

Key Features of Casper Contracts

Casperのプラットフォーム上では、開発者がWasmバイナリーにコンパイルされる言語どれを使ってもスマートコントラクトを書くことができるようにしています。このチュートリアルでは、特に、Rust言語でのスマートコントラクトの書き方に的を絞っています。Rustコンパイラーは、コントラクトのコードをWasmバイナリーにコンパイルしてくれます。その後、put_deployを用いて、Casper Network上のノードにそのWasmバイナリーを送信します。ネットワーク内のノードがデプロイをゴシップし、そしてブロックに取り込みファイナライズします。ファイナライズ後は、ネットワークがブロック内のデプロイを実行します。

更に、Casperプラットフォームではコントラクトのアップグレードが可能です。ContractPackageは、new_contract もしくは new_locked_contract メソッドを介して作成されます。これらのメソッドを通して、Casperの実行エンジンは新しいコントラクトパッケージを自動生成し、ContractPackageHashを割り当てます。新規のコントラクトは、ContractHash キーと一緒にコントラクトパッケージに追加されます。実行エンジンは、コントラクトパッケージ内に新規のコントラクトを格納し、以前にインストールしたコントラクトがある場合は、そのバージョンと並べて表示します。

new_contractnew_locked_contract メソッドは、自動的に新規コントラクトに紐づいたパッケージを生成してくれるので便利です。これらのメソッドを使用しない開発者は、まず初めに、新規コントラクト用のコンテナとして機能するコントラクトパッケージを作成しなければなりません。

コントラクトには必要なメタデータが含まれており、主に ContractHash によって識別されます。コントラクトハッシュは、特定の コントラクトバージョンを識別しますが、 ContractPackageHash は、コントラクトパッケージ内の最新のコントラクトバージョンを識別する役割を果たします。

ディレクトリ構造の作成

スマートコントラクトの作成を開始するには、以下の通りマニュアルもしくは自動でプロジェクトのディレクトリ構造を設定しなくてはいけません。

project-directory/

└── contract/
    ├── src/
        └── main.rs
    └── Cargo.toml

└── Makefile
└── rust-toolchain

└── tests/
    ├── src/
        └── integration-tests.rs
    └── Cargo.toml

フルスタックのアーキテクチャをもつ dApp の場合、プロジェクト構造は異なるでしょう。

cargo casperを使用した自動設定​

cargo casper コマンド は、自動でプロジェクト構造を設定できます。新しいCasperプロジェクトを設定方法として、これを推奨しています。

cargo casper my-project

cargo casperコマンドは、コントラクトのディレクトリ内にサンプルコントラクトとintegration-tests.rsファイルに定義されたロジックを持つサンプルのテストクレートを生成します。Makefileには、コントラクトの準備とビルド用のコマンドとRustの対象ビルドバージョンを指定するrust-toolchainファイルが含まれています。

真っ新なcargoを使ったSemi-auto方式

💡Tip

初心者の方は、cargo casperを使った自動での構造生成をお勧めしており、コマンドがコーディングに必要なものは全て作成してくれます。

1. コントラクトコードとそのテスト対応用として、top-level にプロジェクトディレクトリを作成します。

2. そのプロジェクトディレクトリ内にて以下のコマンドを実行し、contractという名前の新しいバイナリパッケージを作成します。もし他の名前がよければ、他の名前を使用できます。

cargo new contract

コマンドが、 /src/main.rsファイルと Cargo.tomlファイルが含まれているコントラクトフォルダーを生成してくれます。

・main.rs – コントラクトコードが書かれています。

Cargo.toml– クレートの依存関係と他の設定が書かれています。

以下のセクションにて、これらのファイルの更新方法をサンプルコードを使って説明しています。

3. プロジェクトディレクトリ内にて、テスト用フォルダーの自動生成用コマンドを実行します。tests以外の名前がよければ、他の名前を利用できます。

cargo new tests

コマンドが、 /src/main.rsファイルと Cargo.tomlファイルが含まれているコントラクトフォルダーを生成してくれます。

・main.rs – コントラクトのテストに必要なテストコードを格納するファイルです。サンプル構造のように、integration-tests.rsに変更できます。

Cargo.toml– テスト設定用のファイルです。

スマートコントラクトのテストガイドにて、サンプルコードを用いたテストの更新方法について説明しています。

4. cargo casperとは異なり, cargoMakefile や rust-toolchain設定ファイルの作成を行いません。その為、これらのファイルを手動でプロジェクトのルートフォルダーに追加する必要があります。

  • Makefileのサンプル
    prepare:
            rustup target add wasm32-unknown-unknown
    
    build-contract:
            cd contract && cargo build --release --target wasm32-unknown-unknown
            wasm-strip contract/target/wasm32-unknown-unknown/release/contract.wasm 2>/dev/null | true
    
    test: build-contract
            mkdir -p tests/wasm
            cp contract/target/wasm32-unknown-unknown/release/contract.wasm tests/wasm
            cd tests && cargo test
    
    clippy:
            cd contract && cargo clippy --all-targets -- -D warnings
            cd tests && cargo clippy --all-targets -- -D warnings
    
    check-lint: clippy
            cd contract && cargo fmt -- --check
            cd tests && cargo fmt -- --check
    
    lint: clippy
            cd contract && cargo fmt
            cd tests && cargo fmt
  • rust-toolchainファイルのサンプル
    nightly-2022-08-03

マニュアル方式

💡TIP

初心者の方は、cargo casperを使った自動での構造生成をお勧めしており、コマンドがコーディングに必要なものは全て作成してくれます。

1. コントラクトコードとそのテスト対応用として、top-level にプロジェクトディレクトリを作成します。

2. プロジェクトディレクトリ内に、コントラクトコード用のフォルダーを作成します。このフォルダーには、WasmにコンパイルされCasperノードにて実行されるロジックが含まれます。この例では、このフォルダー名を contract とします。もし他の名前がよければ、他の名前を使用できます。

  • contractフォルダー内に、srcとい名前のフォルダーとCargo.toml というコントラクトの依存関係を指定するファイルが追加されます。
  • srcフォルダー内に、コントラクトコードを持つRustファイルを追加します。ここでは、main.rsファイルがそれになります。

3. プロジェクトディレクトリに戻り、コントラクトの機能確認を行えるテスト用のフォルダーを作成します。ここでは、フォルダー名をtests としています。

  • testsフォルダー内に、srcという名前のソース用フォルダーとtests実行に必要な依存関係を指定する Cargo.tomlファイルを追加します。
  • srcフォルダー内に、コントラクトの動作確認を行うRustファイルを追加します。ここでは、integration-tests.rsファイルがそれになります。

4. Semi-automatic 設定の (4.)に従い、Makefile と rust-toolchain を手動で作成します。

Dependencies(依存関係)

contractフォルダー内のCargo.tomlファイルには、依存関係とコントラクトに必要なバージョンふぁ含まれています。最小限としては、casper-contract と casper-typesクレートの最新バージョンをインポートしてください。以下の依存関係やバージョンは、あくまで例である為、ご自分の必要条件に応じて調整を行ってください。

自動設定に従っている場合は、依存関係はCargo.tomlファイルに既に書かれているはずです。ただ、semi-auto設定手動(マニュアル)方式の場合は、依存関係を手動でクレートのCargo.tomlファイルに追加しなくてはいけません。

[dependencies]
# A library for developing Casper network smart contracts.
casper-contract = "1.4.4"
# Types shared by many Casper crates for use on a Casper network.
casper-types = "1.5.0"
  • casper-contract = "1.4.4" – 実行エンジン(EE)のSDKを提供。最新バージョンのクレートは、こちら
  • casper-types = "1.5.0" – 沢山あるCasperクレートが共有するCasperネットワークで使用する型。このクレートは、セッションコードのインタープリットと理解の為、EEに必要です。最新バージョンのクレートは、こちらです。

基本的なスマートコントラクトの書き方

この時点では、contract/src/main.rs (cargo-casperを使用した自動設定)で定義されたデフォルトのサンプルコントラクト、空の contract/src/main.rs ファイル (マニュアルでのプロジェクト設定)、もしくは contract/src/main.rs (semi-auto設定)で定義された Rustの “hello world” プログラム のいずれかを持っています。

このセクションでは、Rustでスマートコントラクトをかくプロセスを1つずつ説明していきます。その為、contract/main.rsファイル内に何か残っていないようクリアにしておいてください。

サンプルコードは、カウンターコントラクトのものを用います。このシンプルなコントラクトでは、Caller(呼び出し元)によってインクリメントと整数の取得が行えます。Casperは、casper_contractクレート内のcontract APIを提供しています。

💡INFO

以下が、Rustで頻繁に使用される重要なSyntaxエレメントです。
Match
Array
Loop
Vector
Functions

快適にRustを書くためには、サンプルの詳細について調べる前にこれらの内容を理解することが重要です。

main.rs ファイルの更新

コントラクトコードを書き始めるに、Wasm実行環境をサポートする以下のファイル属亭を追加します。自動生成のmain.rsファイルを持っている場合は、自動生成のメイン機能を外してください。

#![no_std]
#![no_main]
  • #![no_main] – この属性は、スタンダードのメイン機能をエントリーポイントとして使用しないようプログラムに伝えます。
  • #![no_std] – この属性は、スタンダードライブラリをインポートしないようプログラムに伝えます。

必要な依存関係の定義

必要なインポートと依存関係を追加します。カウンターコントラクトのサンプルコードでは、以下の依存関係を宣言します。

// This code imports necessary aspects of external crates that we will use in our contract code.
extern crate alloc;

// Importing Rust types.
use alloc::{
    string::{String, ToString},
    vec::Vec,
};
// Importing aspects of the Casper platform.
use casper_contract::{
    contract_api::{runtime, storage},
    unwrap_or_revert::UnwrapOrRevert,
};
// Importing specific Casper types.
use casper_types::{
    api_error::ApiError,
    contracts::{EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, NamedKeys},
    CLType, CLValue, URef,
};

グローバル定数の定義

必要な依存関係をインポートした後は、コントラクト自体に使用する定数を定義します。エントリーポイントと値の両方を含みます。下記の例は、私たちが用意したコントラクトに必要な定数の概要となります。

そして、他のインポートしたものと合わせて、main.rsファイルにあるインポートに追加してください。

// Creating constants for values within the contract package.
const CONTRACT_PACKAGE_NAME: &str = "counter_package_name";
const CONTRACT_ACCESS_UREF: &str = "counter_access_uref";

// Creating constants for the various contract entry points.
const ENTRY_POINT_COUNTER_INC: &str = "counter_inc";
const ENTRY_POINT_COUNTER_GET: &str = "counter_get";

// Creating constants for values within the contract.
const CONTRACT_VERSION_KEY: &str = "version";
const CONTRACT_KEY: &str = "counter";
const COUNT_KEY: &str = "count";

コントラクトのエントリーポイントの定義

エントリーポイントは、グローバル・ステートにインストールされたコントラクトコードへのアクセスを可能とします。これらの関数は、セッションコードか他のインスタンスのコントラクトコードによって呼び出されます。コントラクトは、エントリーポイントを少なくとも1つは持っていなくてはならず、1つ以上を持っている場合もあります。コントラクトコードを作成する際、エントリポイントの関数の名前を、実行内容に合わせて明確に定義してください。各エントリーポイントは、昔ながらのプログラムにおける固定のメインエントリーポイントと同等のものです。

エントリーポイントは、関数でもメソッドでもなければ、引数も持っていません。コントラクトのロジックに対して固定したエントリーポイントです。コントラクトロジックは、Deployと一緒に渡された名前によるパラメータへのアクセスが可能です。他のスマートコントラクトがこれらのエントリーポイントにアクセスすることもご留意ください。

エントリーポイントは、1つ以上の必須パラメータをもっており、それが含まれていない場合は戻すロジックがあり、エントリーポイント内で宣言します。オプショナルと重要でないパラメータに関しては、対象ではありません。.

エントリーポイントを定義する際、一行目を #[no_mangle] で始めて、システムがメソッド名内で重要なシンタックスを変更しないようにしてください。各エントリーポイントには、実行したい関数のアクションを派生させるコントラクトコードを含めてください。最後に、ストレージや必要な戻り値を可能であれば含めてください。

以下のエントリーポイントは、カウンターコントラクトの一例です。有効な全てのエントリーポイントについては、GitHubのコントラクトを見直してみてください。

#[no_mangle]
pub extern "C" fn counter_inc() {
    let uref: URef = runtime::get_key(COUNT_KEY)
        .unwrap_or_revert_with(ApiError::MissingKey)
        .into_uref()
        .unwrap_or_revert_with(ApiError::UnexpectedKeyVariant);
    storage::add(uref, 1); // Increment the count by 1.
}

Call(呼び出し)関数の定義

Call関数は、コードの実行から始まり、コントラクトのOn-chainへのインストールを行う関数です。場合によっては、レコードの保存用のDictionaryやパース(お財布)などのいくつかの構造の初期化もます。以下のステップでは、call関数の構成方法についてお伝えします。カウンターコントラクトのCall関数を見直してみてください。

1. 実行引数の定義

コントラクトをインストールする時は、実行引数をパラメーターとして渡します。変数定義のこのパターンを使い、コントラクトの動作を検知する門番となる値を収集します。エントリーポイントが引数を持つ場合、エントリーポイントの定義としてそれらを宣言しなくてはなりません。

引数をとっているエントリーポイントのサンプルとして、CEP-78コントラクトを参照いただけます。カウンターコントラクトはシンプルなものなので、変数パラメータは用いていません。

2. call関数に関数のエントリーポイントを挿入

call関数は、標準的なmain関数を置き換え、自動的にcallerがコントラクトコードと繋がった時に実行します。call関数内では、callerが他のインスタンスのコードを使ってアクセスできるエントリーポイントを定義します。呼び出す為のコードは、セッションかコントラクトコードのインスタンスとなるかもしれません。エントリーポイントを呼び出すコードを書いている時は、エントリーポイント名が1対1でマッピングしていなくてはなりません。そうでないと、実行エンジンは、エントリーポイントが存在しない、とエラーを返してきます。

各エントリーポイントには、下記引数が含まれています。

  • name – エントリーポイントの名前(初期に定義したもの)
  • arguments – エントリーポイントの定義の一部として宣言した実行引数のリスト
  • return type – エントリーポイントから返されるCLType(戻す型が空の場合は、Unit型を使用)
  • access level – エントリーポイントのアクセス許可
  • entry point typecontract もしくは session コード

このステップでは、add_entry_pointメソッドを使った counter_entry_points オブジェクトに個別のエントリーポイントを追加します。このオブジェクトは、後ほど new_contractメソッドに渡されます。

#[no_mangle]
pub extern "C" fn call() {
    // Initialize the count to 0 locally
    let count_start = storage::new_uref(0_i32);
    // Create the entry points for this contract
    let mut counter_entry_points = EntryPoints::new();

    counter_entry_points.add_entry_point(EntryPoint::new(
        ENTRY_POINT_COUNTER_GET,
        Vec::new(),
        CLType::I32,
        EntryPointAccess::Public,
        EntryPointType::Contract,
    ));

    counter_entry_points.add_entry_point(EntryPoint::new(
        ENTRY_POINT_COUNTER_INC,
        Vec::new(),
        CLType::Unit,
        EntryPointAccess::Public,
        EntryPointType::Contract,
    ));
}

下記では、このcall関数にコンテンツを追加していきます。

3. コントラクトの名前付き鍵の生成

NamedKeys(名前付き鍵)は、 いくつかのネットワークデータを素早く識別できる String-Key ペアのコレクションです。

  • Stringは、データを識別する為の名前
  • Keyは、参照されるデータ

他のアカウントやスマートコントラクト、URef、transfer(転送)、デプロイ情報、お財布情報等の記録や値の保存を必要に応じて行う為に、NamedKeysを作成します。 利用可能な鍵の変数は、こちらをご確認ください。

カウンターの場合は、名前付き鍵に対してインクリメントする整数を保存します。

    // In the named keys of the counter contract, add a key for the count.
    let mut counter_named_keys = NamedKeys::new();
    let key_name = String::from(COUNT_KEY);
    counter_named_keys.insert(key_name, count_start.into());
4. コントラクトの作成

new_contract 作成メソッドを使用して、そのnamed keysとエントリーポイントを持つコントラクトを作成します。このメソッドは、コントラクトのオブジェクトを作成し、URefのアクセス権とcallerのコンテキスト内にあるコントラクトパッケージハッシュを保存します。実行エンジンは、自動的にコントラクトパッケージを作成し、contractPackageHashを割り当てます。そして、コントラクトをcontractHashと一緒にパッケージに追加します。

    // Create a new contract package that can be upgraded.
    let (stored_contract_hash, contract_version) = storage::new_contract(
        counter_entry_points,
        Some(counter_named_keys),
        Some(CONTRACT_PACKAGE_NAME.to_string()),
        Some(CONTRACT_ACCESS_UREF.to_string()),
    );

通常は、これらのコントラクトは、新しいversionsを追加してアップグレードすることが可能となっています。新しいバージョンのコントラクトを追加するには、URefにコントラクトパッケージへのアクセス権が必要となります。それは、引数のSome(CONTRACT_ACCESS_UREF.to_string())new_contract メソッドに渡すことで、解決できます。コントラクトのアップグレードを抑止したい場合は、以下にて説明している new_locked_contract メソッドを用います。

5. 追加の名前付き鍵の生成

一般的には、Contract_HashContract_Version が、アカウントのコンテキスト内に後で使用する為にNamedKeys として保存されます。

    // Store the contract version in the context's named keys.
    let version_uref = storage::new_uref(contract_version);
    runtime::put_key(CONTRACT_VERSION_KEY, version_uref.into());

    // Create a named key for the contract hash.
    runtime::put_key(CONTRACT_KEY, stored_contract_hash.into());

Call(呼び出し)機能の全体は、以下のようになっています。

#[no_mangle]
pub extern "C" fn call() {
    // Initialize the count to 0 locally
    let count_start = storage::new_uref(0_i32);
    // Create the entry points for this contract
    let mut counter_entry_points = EntryPoints::new();

    counter_entry_points.add_entry_point(EntryPoint::new(
        ENTRY_POINT_COUNTER_GET,
        Vec::new(),
        CLType::I32,
        EntryPointAccess::Public,
        EntryPointType::Contract,
    ));

    counter_entry_points.add_entry_point(EntryPoint::new(
        ENTRY_POINT_COUNTER_INC,
        Vec::new(),
        CLType::Unit,
        EntryPointAccess::Public,
        EntryPointType::Contract,
    ));

    // In the named keys of the counter contract, add a key for the count.
    let mut counter_named_keys = NamedKeys::new();
    let key_name = String::from(COUNT_KEY);
    counter_named_keys.insert(key_name, count_start.into());

    // Create a new contract package that can be upgraded.
    let (stored_contract_hash, contract_version) = storage::new_contract(
        counter_entry_points,
        Some(counter_named_keys),
        Some(CONTRACT_PACKAGE_NAME.to_string()),
        Some(CONTRACT_ACCESS_UREF.to_string()),
    );

    /* To create a locked contract instead, use new_locked_contract and throw away the contract version returned
    let (stored_contract_hash, _) =
    storage::new_locked_contract(counter_entry_points, Some(counter_named_keys), None, None); */

    // Store the contract version in the context's named keys.
    let version_uref = storage::new_uref(contract_version);
    runtime::put_key(CONTRACT_VERSION_KEY, version_uref.into());

    // Create a named key for the contract hash.
    runtime::put_key(CONTRACT_KEY, stored_contract_hash.into());
}

ロックされたコントラクト

ロックされたコントラクトは、同じコントラクトパッケージ内に他のversionsを持つことはできません。よって、アップグレードはできません。このシナリオでは、Casperの実行エンジンがコントラクトパッケージを作成し、パッケージにコントラクトを追加し、そしてコントラクトへのアップグレードを抑止します。強いセキュリティを必要としていたり、コントラクトのアップデートが不要な場合は、ロックされたコントラクトが使えます。

pub fn new_locked_contract(
    entry_points: EntryPoints,
    named_keys: Option<NamedKeys>,
    hash_name: Option<String>,
    uref_name: Option<String>,
) -> (ContractHash, ContractVersion) {
    create_contract(entry_points, named_keys, hash_name, uref_name, true)
}
  • entry_points – スマートコントラクト内で定義されたエントリーポイントのセット
  • named_keys – コントラクトとペアになったnamed-key
  • hash_name – コントラクトのハッシュ値。hash_nameの現コンテキストの名前付き鍵内に contractHashを置いてください。
  • uref_name – URefのアクセス権の値。uref_nameの現コンテキストの名前付き鍵内に access_uref を置いてください。

Note

現コンテキストとは、通常はアカウントであり、call関数を初期化した人のコンテキストとなります。

サンプルにあるカウンターコントラクトをこの方法で作成した場合は、ロックされます。

let (stored_contract_hash, _) =
        storage::new_locked_contract(counter_entry_points, Some(counter_named_keys), None, None);

コントラクトコードのコンパイル

スマートコントラクトをコンパイルするには、Cargo.tomlファイルとsrcフォルダーからなる自分のプロジェクトディレクトリ内にて以下のコマンドを実行します。

rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown

カウンターのサンプルの場合は、Makefileが置かれているプロジェクトディレクトリ内にて以下のコマンドを実行します。

make prepare
make build-contract

コントラクトコードの実行

コントラクトの実行は、セッションコードや他のスマートコントラクトなどの外部からの呼び出しによって開始します。開発者は、次項で述べるコントラクトコードとセッションコードの違いについても把握しておいてください。

Video Walkthrough

このビデオは、ガイドに沿ったものとなっています。

What’s Next?