認証鍵の使用

⚠CAUTION
このサンプルは、本番環境では使用しないでください。これは、学習用として作成しており、テストの実施や本番環境仕様にする必要があります。

このチュートリアルでは、list_authorization_keys関数を用いたデプロイに紐づいている認証鍵の取得と利用方法についてお見せします。

let authorization_keys = runtime::list_authorization_keys();

認証鍵は、署名者の署名と公開鍵、そして呼び出された認証鍵をリストアップしているDeployの approvals の項目にリストアップされていることを覚えておいてください。以下は、デプロイの承認(approval)についてのサンプルです。

"approvals": [
    {
      "signer": "02021a4da3d6f32ea3ebd2519e1a37a1b811671085bf4f1cf2a36b931344a99b756a",
      "signature": "02df8cdf0bff3bd93e831d24563d5acbefa0ed13814550e910d03208d5fb3c11770dd3d918784ec84342e53666eacf59aeecbf4ce0cdd60e167c4a4b20e4b8f481"
    }
]

この例におけるコントラクトコードは、runtime::list_authorization_keys 関数を呼び出して得られたデプロイの認証鍵のセットを取得します。言い換えると、list_authorization_keys は、デプロイの署名を行った鍵を表すアカウントハッシュのセットを返します。インストールすると、コントラクトコードはNamedKeyにデプロイをインストールする認証鍵を格納します。コントラクトには、呼び出し元デプロイとインストールデプロイの認証鍵のインターセクションを返すエントリーポイントも含まれています。このリポジトリにあるtestsは、異なるシナリオの確認とインターセクションの結果の確認を行います。

前提条件

Workflow

開始するには、tutorials-example-wasmをクローンしてください。そして、authorization-keys-exampleディレクトリを開き、Rust 環境を準備してください。そして、以下のコマンドを用いてtestsをビルドします。

git clone https://github.com/casper-ecosystem/tutorials-example-wasm
cd tutorials-example-wasm/authorization-keys-example
make prepare
make test

リポジトリ構造のレビュー:

  • client – clientフォルダーには2つのWasmファイルが入っています。
    • add_keys.wasm – アソシエイトキーを呼び出しアカウントに追加するセッションコード
    • contract_call.wasm – コントラクトのエントリーポイントの呼び出しと名前付き鍵に結果を保存するセッションコード
  • contract – 認証キーの使用方法のデモや contract.wasm ファイルへのコンパイルを行うシンプルなコントラクト
  • tests – Testsとコントラクトの想定された動作確認デモを行うサポートユーティリティ

💡NOTE
このチュートリアルでは、GitHubに記載しているコードのある行を取りあげています。

コントラクトのサンプル例

installationを行うと、このサンプルのコントラクトは名前付き鍵にインストールしたデプロイに署名を行った認証鍵を格納します。

#[no_mangle]
pub extern "C" fn init() {
    if runtime::get_key(AUTHORIZATION_KEYS_INSTALLER).is_none() {
        let authorization_keys: Vec<AccountHash> =
            runtime::list_authorization_keys().iter().cloned().collect();

        let authorization_keys: Key = storage::new_uref(authorization_keys).into();
        runtime::put_key(AUTHORIZATION_KEYS_INSTALLER, authorization_keys);
    }
}

コントラクトには、呼び出し元のデプロイ用認証鍵とコントラクトのインストール中に保存されるインストーラーデプロイの認証鍵のインターセクションを返すエントリーポイントが含まれています。以下の runtime::list_authorization_keysusage は、呼び出し用デプロイに署名した鍵を表示するアカウントハッシュのセットを取得します。

let authorization_keys_caller: Vec<AccountHash> =
    runtime::list_authorization_keys().iter().cloned().collect();

Client Wasmファイル

add_keys.wasm

このファイルには、呼び出しアカウントにアソシエイトキーを追加するセッションコードが含まれています。詳細についてや似たサンプルは、二社間によるデプロイへの複数署名(マルチシグ)チュートリアルをご確認ください。

contract_call.wasm

このセッションコードは、2つの鍵セット間のインターセクションを返すコントラクトのエントリーポイントを呼び出します。

  • (このチュートリアルでは installer deploy と呼ばれている)コントラクトをインストールしたデプロイに署名した認証鍵
  • (このチュートリアルでは caller deploy と呼ばれている)エントリーポイントを呼び出すデプロイに署名した認証鍵

インターセクション結果は、contract_call.wasmを呼び出すアカウントの名前付き鍵に格納されたリストです。

let key_name: String = runtime::get_named_arg(ARG_KEY_NAME);
let intersection =
    runtime::call_contract::<Vec<AccountHash>>(contract_hash, ENTRY_POINT, runtime_args! {});
runtime::put_key(&key_name, storage::new_uref(intersection).into());
}

サンプルのテスト

このセクションは、認証キーの使用方法のデモ用サンプルテストについてお伝えします。テストは3つのパートに分かれています。

  • コントラクトのインストールのテスト
  • コントラクトのユニークなエントリーポイントのテスト
  • クライアントコントラクトの呼び出しを使用したエントリーポイントのテスト

これらのテストは、コントラクトのインストールに焦点を充てています。

Test 1: should_allow_install_contract_with_default_account

Installer deploy authorization keysExpected outcome
DEFAULT_ACCOUNT_ADDRSuccessful contract installation
(コントラクトのインストールに成功)

この test は、アカウントのアソシエイトキーに属する認証鍵である DEFAULT_ACCOUNT_ADDR による installer deploy への署名を行います。要するに、呼び出し元がデフォルトアカウントの為、DEFAULT_ACCOUNT_ADDR がデプロイへの署名に使われます。

let session_code = PathBuf::from(CONTRACT_WASM);
let session_args = RuntimeArgs::new();

let deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, session_args)
    .build();

Test 2: should_disallow_install_with_non_added_authorization_key

Installer deploy authorization keysExpected outcome
DEFAULT_ACCOUNT_ADDRaccount_addr_1Failed contract installation
(コントラクトのインストールに失敗)

この test では、呼び出し元のアソシエイトキーの一部ではない認証鍵による installer deploy への署名を試みます。それは、デプロイへの署名に使った認証鍵は、呼び出し元のアソシエイトキーのサブセットである必要がある為です。よって、デプロイのインストールは想定通り失敗します。

let session_code = PathBuf::from(CONTRACT_WASM);
let session_args = RuntimeArgs::new();

let deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR, account_addr_1])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, session_args)
    .build();

let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item).build();
builder.exec(execute_request).commit().expect_failure();
let error = builder.get_error().expect("must have error");
assert_eq!(error.to_string(), "Authorization failure: not authorized.");

Test 3: should_allow_install_with_added_authorization_key

Installer deploy authorization keysExpected outcome
DEFAULT_ACCOUNT_ADDRaccount_addr_1Successful contract installation
(コントラクトのインストールに成功)

この test では、追加された認証鍵を用いてデプロイのインストールを成功させるデモを行います。テストフレームワークの初期設定後、テストがセッションコードを呼び出し、アソシエイトアカウントである account_addr_1 とデフォルトアカウントのアソシエイトキーに追加します。

// Add account_addr_1 to the default account's associated keys
let session_code = PathBuf::from(ADD_KEYS_WASM);
let session_args = runtime_args! {
    ASSOCIATED_ACCOUNT => account_addr_1
};

let add_keys_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, session_args)
    .build();

let add_keys_execute_request =
    ExecuteRequestBuilder::from_deploy_item(add_keys_deploy_item).build();

builder
    .exec(add_keys_execute_request)
    .commit()
    .expect_success();

デプロイの閾値が現在2に設定されている為、installer deployはデフォルトのアカウントハッシュと account_addr_1 によって署名されます。GitHubをご確認ください。

let session_code = PathBuf::from(CONTRACT_WASM);

let deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR, account_addr_1])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, session_args)
    .build();

let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item).build();
builder.exec(execute_request).commit().expect_success();

次のテストでは、呼び出し元デプロイの認証鍵とインストールデプロイの認証鍵間のインターセクションを計算するコントラクトのユニークエントリポイントの実行を行います。

Test 4: should_allow_entry_point_with_installer_authorization_key

Installer deploy authorization keysCaller deploy authorization keysIntersection returned by the entry point
DEFAULT_ACCOUNT_ADDRaccount_addr_1DEFAULT_ACCOUNT_ADDRaccount_addr_1

この test は、デフォルトアカウントのアソシエイトキーにアソシエイトアカウントを追加し、その2つの鍵を使ってコントラクトをインストールした前回のテスト上にビルドします。更には、201行目で test は account_addr_1 で署名した ACCOUNT_USER_1 にて実行するデプロイを用いたコントラクトのエントリーポイントを呼び出します。ACCOUNT_USER_1 のデプロイアクション閾値がここに設定されている通り1である為、これが可能なのです。

let contract_hash = builder
    .get_expected_account(*DEFAULT_ACCOUNT_ADDR)
    .named_keys()
    .get(CONTRACT_HASH)
    .expect("must have this entry in named keys")
    .into_hash()
    .map(ContractHash::new)
    .unwrap();

let entry_point_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[account_addr_1])
    .with_address(account_addr_1)
    .with_stored_session_hash(contract_hash, ENTRYPOINT, runtime_args! {})
    .build();

let entry_point_request =
    ExecuteRequestBuilder::from_deploy_item(entry_point_deploy_item).build();

builder.exec(entry_point_request).expect_success().commit();

エントリーポイントは、呼び出し元デプロイの認証鍵とインストールデプロイの認証鍵のインターセクションを返します。インターセクションは、account_addr_1鍵が含まれたリストです。よって、呼び出し元デプロイは、成功する想定の上で結果を返します。

Test 5: should_allow_entry_point_with_account_authorization_key

Installer deploy authorization keysCaller deploy authorization keysIntersection returned by the entry point
DEFAULT_ACCOUNT_ADDRaccount_addr_1DEFAULT_ACCOUNT_ADDRDEFAULT_ACCOUNT_ADDR

このサンプルリポジトリの main test となります。デフォルトアカウントを使ってコントラクトをインストール後、test は、アソシエイトキーとしてデフォルトアカウントハッシュを ACCOUNT_USER_1 に追加します。

let session_code = PathBuf::from(ADD_KEYS_WASM);
let session_args = runtime_args! {
    ASSOCIATED_ACCOUNT => *DEFAULT_ACCOUNT_ADDR
};

let add_keys_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[account_addr_1])
    .with_address(account_addr_1)
    .with_session_code(session_code, session_args)
    .build();

そして、test がデプロイを作成しコントラクトのエントリーポイントを呼び出します。このデプロイは、ACCOUNT_USER_1 にて実行され、account_addr_1とデフォルトアカウントハッシュの2つの認証鍵を持っています。デプロイのアクション閾値は2に設定されている為、それを満たすには認証鍵2つ共がデプロイに署名する必要があります。結果のインターセクションはデフォルトアカウントハッシュを持っているので、デプロイ実行は成功するはずです。

let entry_point_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[account_addr_1, *DEFAULT_ACCOUNT_ADDR])
    .with_address(account_addr_1)
    .with_stored_session_hash(contract_hash, ENTRYPOINT, runtime_args! {})
    .build();

let entry_point_request =
    ExecuteRequestBuilder::from_deploy_item(entry_point_deploy_item).build();

builder.exec(entry_point_request).expect_success().commit();

Test 6: should_disallow_entry_point_without_authorization_key

Installer deploy authorization keysCaller deploy authorization keysIntersection returned by the entry point
DEFAULT_ACCOUNT_ADDRaccount_addr_2None

この test は、呼び出し元デプロイの認証鍵とインストールデプロイの認証鍵の間のインターセクションが存在しない場合は、エントリーポイントがエラーを返すことを確認します。

デフォルトのアカウントハッシュは、installer deployへの署名に使用されます。

let session_code = PathBuf::from(CONTRACT_WASM);

let deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, runtime_args! {})
    .build();

test には、新規アカウントである ACCOUNT_USER_2 がコントラクトのエントリーポイントを呼び出すデプロイを作成し、account_addr_2 を用いてデプロイに署名します。エントリーポイントを呼び出すと、エラーが返されます。なぜなら、呼び出し元とinstaller deployが共通の認証鍵を持っていないからです。

    // Here ACCOUNT_USER_2 does not have DEFAULT_ACCOUNT_ADDR (from the contract installer) in its associated keys
    // The deploy will therefore revert with PermissionDenied
    let entry_point_deploy_item = DeployItemBuilder::new()
        .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
        .with_authorization_keys(&[account_addr_2])
        .with_address(account_addr_2)
        .with_stored_session_hash(contract_hash, ENTRYPOINT, runtime_args! {})
        .build();

    let entry_point_request =
        ExecuteRequestBuilder::from_deploy_item(entry_point_deploy_item).build();

    builder.exec(entry_point_request).commit().expect_failure();
    let error = builder.get_error().expect("must have User error: 0");
    assert_expected_error(
        error,
        0,
        "should fail execution since DEFAULT_ACCOUNT_ADDR is not in ACCOUNT_USER_2 associated keys",
    );

以下テストでは、contract call を用いたエントリーポイントの実行練習と返された結果の確認を行います。

Test 7: should_allow_entry_point_through_contract_call_with_authorization_key

Installer deploy authorization keysCaller deploy authorization keysIntersection returned by the entry point
DEFAULT_ACCOUNT_ADDRaccount_addr_1DEFAULT_ACCOUNT_ADDRDEFAULT_ACCOUNT_ADDR

この test は、クライアントによるコントラクトの呼び出しにて、コントラクトのエントリーポイントを検証します。コントラクトは、デフォルトのアカウントハッシュにてデプロイの認証鍵にインストールされます。

let session_code = PathBuf::from(CONTRACT_WASM);

let deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[*DEFAULT_ACCOUNT_ADDR])
    .with_address(*DEFAULT_ACCOUNT_ADDR)
    .with_session_code(session_code, runtime_args! {})
    .build();

呼び出し元のデプロイ(caller deploy)は、account_addr_1 and DEFAULT_ACCOUNT_ADDR によって署名されます。

let entry_point_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[account_addr_1, *DEFAULT_ACCOUNT_ADDR])
    .with_address(account_addr_1)
    .with_session_code(session_code, session_args)
    .build();

let entry_point_request =
    ExecuteRequestBuilder::from_deploy_item(entry_point_deploy_item).build();
builder.exec(entry_point_request).expect_success().commit();

そこで、testは、返された結果がデフォルトのアカウントハッシュを持つ ACCOUNT_USER_1の名前付き鍵に保存されたことを確認します。

let intersection_receipt: Key = *builder
    .get_expected_account(account_addr_1)
    .named_keys()
    .get(INTERSECTION_RECEIPT)
    .expect("must have this entry in named keys");

let actual_intersection = builder
    .query(None, intersection_receipt, &[])
    .expect("must have stored_value")
    .as_cl_value()
    .map(|intersection_cl_value| {
        CLValue::into_t::<Vec<AccountHash>>(intersection_cl_value.clone())
    })
    .unwrap()
    .unwrap();

let expected_intersection = vec![*DEFAULT_ACCOUNT_ADDR];

assert_eq!(actual_intersection, expected_intersection);

Test 8: should_disallow_entry_point_through_contract_call_without_authorization_key

Installer deploy authorization keysCaller deploy authorization keysIntersection returned by the entry point
DEFAULT_ACCOUNT_ADDRaccount_addr_1account_addr_2None

このチュートリアルの final test が呼び出し元デプロイの認証鍵(account_addr_1account_addr_2)とインストーラデプロイの認証鍵(DEFAULT_ACCOUNT_ADDR)間にインターセクションが存在しないことを確認した場合は、エントリーポイントがエラーを返します。

 let session_code = PathBuf::from(CONTRACT_CALL_WASM);

let session_args = runtime_args! {
    ARG_CONTRACT_HASH => Key::from(contract_hash),
    ARG_KEY_NAME => INTERSECTION_RECEIPT
};

// account_addr_2 as an associated key is not among the default account's associated keys
// The deploy will therefore revert with PermissionDenied
let entry_point_deploy_item = DeployItemBuilder::new()
    .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT})
    .with_authorization_keys(&[account_addr_1, account_addr_2])
    .with_address(account_addr_1)
    .with_session_code(session_code, session_args)
    .build();

let entry_point_request =
    ExecuteRequestBuilder::from_deploy_item(entry_point_deploy_item).build();

builder.exec(entry_point_request).commit().expect_failure();

let error = builder.get_error().expect("must have User error: 0");
assert_expected_error(
    error,
    0,
    "should fail execution since ACCOUNT_USER_2 as associated key is not in installer (DEFAULT_ACCOUNT_ADDR) associated keys",
);