スマートコントラクトのアップグレード

このチュートリアルでは、他のソフトウェアのアップグレードに似た手法の既存コントラクトのアップグレード方法を検証します。新規コントラクトの追加とコントラクトパッケージが使用するべきデフォルトコントラクトバージョンの更新を行うと、アンロックされたコントラクトパッケージを変更できます。コントラクトパッケージハッシュの把握とadd_contract_version APIの使用が必要となります。バージョン管理されないロックされたコントラクトパッケージを作成することも可能ですが、バージョン管理されない為アップグレードはできません。

Video Tutorial

このビデオは、このチュートリアルを動画でお伝えしています。

Prerequisites(事前準備)

💡NOTE
コントラクト (contract-v2.wasm) の2番目のバージョンをインストールする前に、カウンターコントラクトのチュートリアル にてお伝えしたコントラクト (contract-v1.wasm) の最初のバージョンをインストールすることが前提条件となっています。

コードを見てみると、異なるバージョンのコントラクトを確認できます。

  • contract-v1は、Testnet上のCounterコントラクト チュートリアルにあるカウンターコントラクトです。
  • contract-v2 は、新しい counter_decrement エントリーポイントを持つコントラクトです。

コントラクトのバージョニングフロー

以下は、バージョン管理されたコントラクトパッケージを作成するワークフローの一例です。既にコントラクトパッケージを作成し、そのハッシュを使っている場合は、ワークフローは異なります。

  1. new_contract を使用した最も一般的な方法にてコントラクトを作成します。
  2.  add_contract_version を使用してコントラクトの新しいバージョンを追加します。
  3. 新規コントラクトを作成し、対象の .wasm ファイルを生成します。
  4. デプロイを介して、ネットワーク上にコントラクトをインストールします。
  5. 新規コントラクトバージョンが、想定通りに動作することを確認します。

このチュートリアルでは、カウンターコントラクトの second version を使用してアップグレードを行います。

Step 1. 新規アンロックコントラクトの作成

new_contract 関数を使用して新規コントラクトを作成し、後からアクセスする為、グローバルステートにある鍵から返された ContractHash を保存します。内部では、実行エンジンがバージョン管理可能なコントラクトパッケージ(コントラクトの入れ物)を作成します。

コントラクトの作成時に、パッケージ名の指定と追加修正用にURefへアクセスできます。URefへのアクセスキーが不明の場合、セキュリティ上、新しいコントラクトバージョンの追加はできません。オプションとして、名前付き鍵にてコントラクトパッケージの最新バージョンを保存することも可能です。

    // Create a new contract and specify a package name and access URef for further modifications
    let (stored_contract_hash, contract_version) = storage::new_contract(
        contract_entry_points,
        Some(contract_named_keys),
        Some("contract_package_name".to_string()),
        Some("contract_access_uref".to_string()),
    );

    // The hash of the installed contract will be reachable through a named key
    runtime::put_key(CONTRACT_KEY, stored_contract_hash.into());

    // The current version of the contract will be reachable through a named key
    let version_uref = storage::new_uref(contract_version);
    runtime::put_key(CONTRACT_VERSION_KEY, version_uref.into());

カウンターコントラクトの最初のバージョンが、バージョン管理が可能なコントラクトパッケージであることをお伝えしています。このステップは、Testnet上のカウンターコントラクトのチュートリアルに書かれています。

追加の詳細情報:

  1. バージョン管理しているのはコントラクトパッケージであり、コントラクトではありません。コントラクトは、常に設定したバージョンのままであり、パッケージにて使用するコントラクトバージョンを指定します。
  2. 新しいコントラクトのWasmファイル名は、ネットワークへのデプロイ送信後、コントラクトパッケージのハッシュが別のコントラクトバージョンに紐づき、古いコントラクトとは異なります。

Step 2. 新規コントラクトのパッケージへの追加

新しいエントリーポイントの追加や既存のエントリーポイントの動作の修正、もしくはコントラクトの書き換えなどCasperのコントラクトに変更を加えることは多いと思います。

パッケージに新しいコントラクトのバージョンを追加するには、add_contract_version 関数を呼び出し、ContractPackageHash や EntryPoints、そして NamedKeys に渡します。カウンターのサンプルでは、ここで呼んでいる add_contract_version を確認いただけます。

    let (contract_hash, contract_version) =
    storage::add_contract_version(contract_package_hash,
                                  entry_points,
                                  named_keys);

引数について:

  • contract_package_hash – このハッシュは、コントラクトパッケージに直接紐づきます。ハッシュと鍵についての説明 をご確認ください。
  • entry_points – 修正および新規追加が可能なコントラクトのエントリーポイント
  • named_keys – コントラクトの名前付き鍵

新しいコントラクトバージョンでは、前のバージョンの名前付き鍵を引き継ぎます。新しく名前付き鍵を指定する場合は、新しいコントラクトバージョンにて古い名前付き鍵を変換します。古いコントラクトバージョンと新しいものは、同じ名前付き鍵を使用し、コントラクトの新しいバージョンには、新しい値が表示されます。

コントラクトのバージョン管理は、ユーザーが古いバージョンを使用することを考慮すると必要です。やり方はいくつかあります。

  1. 特定のバージョンのコントラクトハッシュにクライアントコントラクトをピン留めする
  2. クライアントコントラクトをピン留めするバージョンNo. にて call_versioned_contract を使用する
  3. call_versioned_contract と、コントラクトの最新バージョンを使用する、バージョン “None”を使用してコントラクトを呼び出す。

Step 3. コントラクトWasmの利用

以下のコマンドを使用して新規追加のコントラクトの準備とビルドを行います。

make prepare

make build-contract

Step 4. コントラクトのインストール

デプロイを介したネットワークへの コントラクトのインストール とデプロイステータスを確認します。自分のデプロイが受理されたかの確認のため、イベントストリームの監視をします。

アップグレードワークフローを見るには、チェーン上に second contract versionをインストールします。このバージョンには、エントリーポイント counter_decrement が組み込まれています。

💡NOTE
カウンターコントラクトのチュートリアルでお伝えしたコントラクトの最初のバージョンのインストールは、second versionのインストール前の事前準備です。

casper-client put-deploy \
    --node-address http://[NODE_IP]:7777 \
    --chain-name [CHAIN_NAME] \
    --secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \
    --payment-amount [PAYMENT_AMOUNT_IN_MOTES] \
    --session-path [PATH]/contract-v2/target/wasm32-unknown-unknown/release/counter-v2.wasm

Step 5. 変更への認証

ユニットテストを書き、call_contract や call_versioned_contract にて新しいコントラクトバージョンの動作確認を行います。新しいコントラクトを(有効である最新バージョンにインクリメントする)パッケージに追加する際、コントラクトのプライマリ識別子である新しいコントラクトハッシュを取得します。コントラクトハッシュの call_contract を使用できます。他手法として、call_versioned_contract を使用し、contract_package_hash と新規で追加したバージョンを指定できます。

上記のシンプルなカウンターコントラクトのインテグレーションテストをご用意しています。そのテストがどの様に contract’s version とエントリーポイントの保存と確認を行うのか、を知ることができます。

ここに記載のある通り、NamedKeyに最新バージョンのコントラクトパッケージを保存します。すると、NamedKeyをクエリし最新バージョンのコントラクトパッケージを確認できます。

  • test関数のサンプル
        // Verify the contract version is now 2.
        let account = builder
            .get_account(*DEFAULT_ACCOUNT_ADDR)
            .expect("should have account");
    
        let version_key = *account
            .named_keys()
            .get(CONTRACT_VERSION_KEY)
            .expect("version uref should exist");
    
        let version = builder
            .query(None, version_key, &[])
            .expect("should be stored value.")
            .as_cl_value()
            .expect("should be cl value.")
            .clone()
            .into_t::()
            .expect("should be u32.");
    
        assert_eq!(version, 2);

Rust コマンドラインクライアントを使って新しいエントリーポイントのテストも行えます。

新規 state-root-hashを取得します。

casper-client get-state-root-hash --node-address http://[NODE_IP]:7777

新規コントラクトのエントリーポイントを確認します。エントリーポイント counter_decrement が、確認できるようになったと思います。

casper-client query-global-state \
    --node-address http://[NODE_IP]:7777 \
    --state-root-hash [STATE_ROOT_HASH] \
    --key [ACCOUNT_HASH] -q "counter"
  • 出力サンプル
     {
      "id": 5602352547578277096,
      "jsonrpc": "2.0",
      "result": {
        "api_version": "1.4.13",
        "block_header": null,
        "merkle_proof": "[54054 hex chars]",
        "stored_value": {
          "Contract": {
            "contract_package_hash": "contract-package-wasmc014187ccf3366cca70317d6d567cd56a05ecf1ee50ed3bd02727c2864e3d3a8",
            "contract_wasm_hash": "contract-wasm-64d252f1ab72c7295a85d15c3f456f8bdda586580b0b7106e203fa4fd83f05d7",
            "entry_points": [
              {
                "access": "Public",
                "args": [],
                "entry_point_type": "Contract",
                "name": "counter_decrement",
                "ret": "Unit"
              },
              {
                "access": "Public",
                "args": [],
                "entry_point_type": "Contract",
                "name": "counter_get",
                "ret": "I32"
              },
              {
                "access": "Public",
                "args": [],
                "entry_point_type": "Contract",
                "name": "counter_inc",
                "ret": "Unit"
              }
            ],
            "named_keys": [
              {
                "key": "uref-ca980a2e4c08dc3f233b728b22b909cd4e894295155a7902bf88a59eac1531d1-007",
                "name": "count"
              }
            ],
            "protocol_version": "1.4.13"
          }
        }
      }
    }

最新のステート・ルート・ハッシュにて、バージョンとパッケージの詳細を確認します。

casper-client query-global-state \
    --node-address http://[NODE_IP]:7777 \
    --state-root-hash [STATE_ROOT_HASH] \
    --key [ACCOUNT_HASH] -q "version"
  • 出力サンプル
    {
      "id": 9084525900533244372,
      "jsonrpc": "2.0",
      "result": {
        "api_version": "1.4.13",
        "block_header": null,
        "merkle_proof": "[64874 hex chars]",
        "stored_value": {
          "CLValue": {
            "bytes": "02000000",
            "cl_type": "U32",
            "parsed": 2
          }
        }
      }
casper-client query-global-state \
    --node-address http://[NODE_IP]:7777 \
    --state-root-hash [STATE_ROOT_HASH] \
    --key [ACCOUNT_HASH] -q "counter_package_name"
  • 出力サンプル
    {
      "id": 6933525663267881367,
      "jsonrpc": "2.0",
      "result": {
        "api_version": "1.4.13",
        "block_header": null,
        "merkle_proof": "[52174 hex chars]",
        "stored_value": {
          "ContractPackage": {
            "access_key": "uref-101817ffd5aa47b08ca710649dbdc41edf0a20d7802c736d34053656c0a99eaf-007",
            "disabled_versions": [],
            "groups": [],
            "versions": [
              {
                "contract_hash": "contract-4ee8a4cfbc0a183d189611b6a14c0f7b57e7635fa17a8acfc5c645fec4c36316",
                "contract_version": 1,
                "protocol_version_major": 1
              },
              {
                "contract_hash": "contract-2cd9f6485423ba846fae83729016b03df26d9babb939466906c8f1d168b40949",
                "contract_version": 2,
                "protocol_version_major": 1
              }
            ]
          }
        }
      }
    }

新規エントリーポイントである counter_decrement を、パッケージ名を使って呼び出し、結果を確認します。

casper-client put-deploy \
    --node-address http://[NODE_IP]:7777 \
    --chain-name [CHAIN_NAME] \
    --secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \
    --payment-amount [PAYMENT_AMOUNT_IN_MOTES] \
    --session-package-name "counter_package_name" \
    --session-entry-point "counter_decrement"

💡NOTE
バージョン管理されたコントラクトを呼び出す方法は2つあります。
1. パッケージハッシュを使ったコントラクトの呼び出し
2. パッケージ名を使ったコントラクトの呼び出し

エントリーポイントを呼び出した後、カウントの値は減っているはずです。それは、新規ステート・ルート・ハッシュを用いたネットワークの再クエリにて確認できます。

コントラクトバージョンの無効化

disable_contract_version 関数を用いると、表示したコントラクトパッケージのコントラクトバージョンを無効にできます。

無効化されたコントラクトバージョンは実行できなくなります。その様に、パッケージ内が1つのコントラクトバージョンのみの場合、そのコントラクトを利用することはできなくなります。

Enable_contract_version は、以前無効にしたコントラクトバージョンを再開することを可能にします。

💡NOTE
コントラクトパッケージの呼び出しは一番最近のコントラクトバージョンが使用されることをご留意くださいませ。特別な理由がない限り、以前のコントラクトバージョンを無効にする必要はありません。

ロックされたコントラクトパッケージの作成

new_locked_contract 関数を用いると、ロックされたコントラクトパッケージを作成できます。このコントラクトのアップグレードは不可となっています。

let (stored_contract_hash, _) = storage::new_locked_contract(
    contract_entry_points,
    Some(contract_named_keys),
    Some("contract_package_name".to_string()),
    Some("contract_access_uref".to_string()),
);

コントラクトエントリーポイントと関数を呼び出す際の名前付き鍵を適用します。コンテキストの名前付き鍵に組み込む hash_name と uref_name を指定することも可能です。コントラクトパッケージのバージョンは常に1に等しい為、返されたバージョンNo. を保存する必要はありません。

💡NOTE
ロックされたコントラクトパッケージを作成するということは、元には戻す必要がない、ということです。コントラクトをアップグレードするには、Step 1で述べたように new_contract を使用します。