機能パリティテスト

プログラムをテストするとき、さまざまなクラスターで同じように実行されることを保証することは、品質と期待される結果の生成の両方にとって不可欠です。

概要

Fact Sheet

  • Featuresとは、Solana バリデーターに導入され、使用するにはアクティベーションが必要な機能です。
  • Featuresはあるクラスター (例: testnet) でアクティブ化される場合がありますが、別のクラスター (例: mainnet-beta) ではアクティブ化されません。
  • しかし、 solana-test-validator をローカルで実行すると、Solana バージョンで利用可能なすべての機能が自動的に有効になります。Solanaバージョンは自動的にアクティベートされます。その結果、ローカルでテストする場合、テストの機能と結果は、別のクラスターで展開して実行する場合と同じではない可能性があります。

シナリオ

3 つの命令を含むトランザクションがあり、各命令が約 100_000 計算ユニット (CU) を消費するとします。Solana 1.8.x バージョンで実行すると、次のような命令 CU 消費が観察されます。:

InstructionStarting CUExecutionRemaining CU
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_000

Solana 1.9.2 では、トランザクションがデフォルトで 200_000 CU 予算を持ち、カプセル化された命令がそのトランザクション予算から 引き出される 'transaction wide compute cap(トランザクション全体の計算上限)' と呼ばれる機能が導入されました。上記と同じトランザクションを実行すると、動作が大きく異なります:

InstructionStarting CUExecutionRemaining CU
1200_000-100_000100_000
2100_000-100_0000
30FAIL!!!FAIL!!!

Yikes!これを知らなかった場合は、これを引き起こすようなインストラクションの挙動変更がなかったため、イライラする可能性があります。ローカルでは失敗してしまった!?

トランザクションの予算を300_000CUに増やすことで、正気を保つことができます。and salvage your sanity しかし、これは**機能パリティ** を使ったテストが、混乱を避けるための積極的な方法であることを示しています。

機能ステータス

solana feature statusコマンドを使用して、特定のクラスターで有効になっている機能を簡単に確認できます。

solana feature status -ud   // Displays by feature status for devnet
solana feature status -ut   // Displays for testnet
solana feature status -um   // Displays for mainnet-beta
solana feature status -ul   // Displays for local, requires running solana-test-validator

または、scfsd などのツールを使用して、表示されるクラスター全体のすべての機能の状態を観察することもできます。これは、ここに示されている部分的な画面であり、 solana-test-validatorを実行する必要はありません。:

Feature Status Heatmap

パリティテスト

上記のように、solana-test-validatorすべての機能を自動的に有効にします。では、「devnet、testnet、または mainnet-beta と同等の環境でローカルにテストするにはどうすればよいですか?」という問いにはどうすべきでしょうか?

解決策: Solana 1.9.6 に PR が追加され、機能を非アクティブ化できるようになりました:

solana-test-validator --deactivate-feature <FEATURE_PUBKEY> ...

簡単なデモンストレーション

エントリポイントで受信したデータをログに記録する単純なプログラムがあるとします。また、プログラムに 2 つのインストラクションを実行するトランザクションをテストしています。

アクティブなすべての機能

  1. 1 つのターミナルでテスト バリデータを起動します:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program ADDRESS target/deploy/PROGNAME.so --reset`
  1. 別のターミナルでログストリーマーを開始します:
solana logs
  1. 次に、トランザクションを実行します。ログターミナルにも同様のものが表示されます (わかりやすくするために編集されています)。:
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[1]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 187157 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success[

私たちの機能 'transaction wide compute cap'は自動的に有効になっており、200_000CUのトランザクションバジェットから、各命令がCUを引き出していることがわかります。

非アクティブな選択機能

  1. この実行では、CU 予算の動作が devnet で実行されているものと同等になるように実行したいと考えています。 Feature Statusで説明されているツールを使用して、 transaction wide compute cap 公開キーを分離し、テスト バリデータの起動時に --deactivate-feature を使用します。
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. 現在、すべてのアップストリーム クラスターの状態である独自の 200_000 CU 予算 (わかりやすくするために編集されています) が命令にあることがログに表示されます:
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success

完全なパリティテスト

solana-test-validator を呼び出す際、まだアクティブでない各機能を特定してそれぞれに--deactivate-feature <FEATURE_PUBKEY>を追加することで、特定のクラスタと同等の振る舞いが可能です:

solana-test-validator --deactivate-feature PUBKEY_1 --deactivate-feature PUBKEY_2 ...

また、scfsd はコマンド スイッチを提供して、クラスターが solana-test-validator スタートアップに直接フィードするための完全な非アクティブ化された機能セットを出力します。:

solana-test-validator -l ./.ledger $(scfsd -c devnet -k -t)

バリデーターの実行中に別のターミナルを開き、solana feature statusを見ると、devnet で非アクティブ化されている機能が非アクティブ化されていることがわかります。

プログラムでの完全パリティテスト

テスト コード内でテスト バリデーターの実行を制御する場合は、TestValidatorGenesis を使用して、テスト バリデーターのアクティブ化/非アクティブ化機能を変更できます。 Solana 1.9.6 では、これをサポートする関数がバリデータ ビルダーに追加されました。

プログラム フォルダーのルートにtestsフォルダーを作成し、parity_test.rsファイルを追加します。 以下は、各テストで使用されるボイラープレース関数 (ボイラープレート) です。

Press </> button to view full source
#[cfg(test)]
mod tests {
    use std::{error, path::PathBuf, str::FromStr};

    // Use gadget-scfs to get interegate feature lists from clusters
    // must have `gadgets-scfs = "0.2.0" in Cargo.toml [dev-dependencies] to use
    use gadgets_scfs::{ScfsCriteria, ScfsMatrix, SCFS_DEVNET};
    use solana_client::rpc_client::RpcClient;
    use solana_program::{instruction::Instruction, message::Message, pubkey::Pubkey};
    use solana_sdk::{
        // Added in Solana 1.9.2
        compute_budget::ComputeBudgetInstruction,
        pubkey,
        signature::{Keypair, Signature},
        signer::Signer,
        transaction::Transaction,
    };
    // Extended in Solana 1.9.6
    use solana_test_validator::{TestValidator, TestValidatorGenesis};

    /// Location/Name of ProgramTestGenesis ledger
    const LEDGER_PATH: &str = "./.ledger";
    /// Path to BPF program (*.so) change if needed
    const PROG_PATH: &str = "target/deploy/";
    /// Program name from program Cargo.toml
    /// FILL IN WITH YOUR PROGRAM_NAME
    const PROG_NAME: &str = "PROGRAM_NAME";
    /// Program public key
    /// FILL IN WITH YOUR PROGRAM'S PUBLIC KEY str
    const PROG_KEY: Pubkey = pubkey!("PROGRAMS_PUBLIC_KEY_BASE58_STRING");
    /// 'transaction wide compute cap' public key
    const TXWIDE_LIMITS: Pubkey = pubkey!("5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9");

    /// Setup the test validator passing features
    /// you want to deactivate before running transactions
    pub fn setup_validator(
        invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
        // Extend environment variable to include our program location
        std::env::set_var("BPF_OUT_DIR", PROG_PATH);
        // Instantiate the test validator
        let mut test_validator = TestValidatorGenesis::default();
        // Once instantiated, TestValidatorGenesis configuration functions follow
        // a builder pattern enabling chaining of settings function calls
        let (test_validator, kp) = test_validator
            // Set the ledger path and name
            // maps to `solana-test-validator --ledger <DIR>`
            .ledger_path(LEDGER_PATH)
            // Load our program. Ignored if reusing ledger
            // maps to `solana-test-validator --bpf-program <ADDRESS_OR_PATH BPF_PROGRAM.SO>`
            .add_program(PROG_NAME, PROG_KEY)
            // Identify features to deactivate. Ignored if reusing ledger
            // maps to `solana-test-validator --deactivate-feature <FEATURE_PUBKEY>`
            .deactivate_features(&invalidate_features)
            // Start the test validator
            .start();
        Ok((test_validator, kp))
    }

    /// Convenience function to remove existing ledger before TestValidatorGenesis setup
    /// maps to `solana-test-validator ... --reset`
    pub fn clean_ledger_setup_validator(
        invalidate_features: Vec<Pubkey>,
    ) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
        if PathBuf::from_str(LEDGER_PATH).unwrap().exists() {
            std::fs::remove_dir_all(LEDGER_PATH).unwrap();
        }
        setup_validator(invalidate_features)
    }

    /// Submits a transaction with programs instruction
    /// Boiler plate
    fn submit_transaction(
        rpc_client: &RpcClient,
        wallet_signer: &dyn Signer,
        instructions: Vec<Instruction>,
    ) -> Result<Signature, Box<dyn std::error::Error>> {
        let mut transaction =
            Transaction::new_unsigned(Message::new(&instructions, Some(&wallet_signer.pubkey())));
        let recent_blockhash = rpc_client
            .get_latest_blockhash()
            .map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
        transaction
            .try_sign(&vec![wallet_signer], recent_blockhash)
            .map_err(|err| format!("error: failed to sign transaction: {}", err))?;
        let signature = rpc_client
            .send_and_confirm_transaction(&transaction)
            .map_err(|err| format!("error: send transaction: {}", err))?;
        Ok(signature)
    }
    // UNIT TEST FOLLOWS
}

これで、mod test {...}の本体にテスト関数を追加して、デフォルトのバリデータ設定(すべての機能が有効になっている状態)と、 コマンドラインからsolana-test-validatorを実行する前の例のようなtransaction wide compute capのデモが可能になりました。

#[test]
fn test_base_pass() {
    // Run with all features activated (default for TestValidatorGenesis)
    let inv_feat = vec![];
    // Start validator with clean (new) ledger
    let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
    // Get the RpcClient
    let connection = test_validator.get_rpc_client();
    // Capture our programs log statements
    solana_logger::setup_with_default("solana_runtime::message=debug");

    // This example doesn't require sending any accounts to program
    let accounts = &[];
    // Build instruction array and submit transaction
    let txn = submit_transaction(
        &connection,
        &main_payer,
        // Add two (2) instructions to transaction to demonstrate
        // that each instruction CU draws down from default Transaction CU (200_000)
        // Replace with instructions that make sense for your program
        [
            Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
            Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
        ]
        .to_vec(),
    );
    assert!(txn.is_ok());
}
#[test]
fn test_deactivate_tx_cu_pass() {
    // Run with all features activated except 'transaction wide compute cap'
    let inv_feat = vec![TXWIDE_LIMITS];
    // Start validator with clean (new) ledger
    let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
    // Get the RpcClient
    let connection = test_validator.get_rpc_client();
    // Capture our programs log statements
    solana_logger::setup_with_default("solana_runtime::message=debug");

    // This example doesn't require sending any accounts to program
    let accounts = &[];
    // Build instruction array and submit transaction
    let txn = submit_transaction(
        &connection,
        &main_payer,
        [
            // This instruction adds CU to transaction budget (1.9.2) but does nothing
            // when we deactivate the 'transaction wide compute cap' feature
            ComputeBudgetInstruction::request_units(400_000u32),
            // Add two (2) instructions to transaction
            // Replace with instructions that make sense for your program
            // You will see that each instruction has the 1.8.x 200_000 CU per budget
            Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
            Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
        ]
        .to_vec(),
    );
    assert!(txn.is_ok());
}

また、scfs engine gadget は、クラスターの非アクティブ化された機能の完全なベクトルを生成できます。 以下は、そのエンジンを使用して、devnet の非アクティブ化されたすべての機能のリストを取得する方法を示しています。

#[test]
fn test_devnet_parity_pass() {
    // Use gadget-scfs to get all deactivated features from devnet
    // must have `gadgets-scfs = "0.2.0" in Cargo.toml to use
    // Here we setup for a run that samples features only
    // from devnet
    let mut my_matrix = ScfsMatrix::new(Some(ScfsCriteria {
        clusters: Some(vec![SCFS_DEVNET.to_string()]),
        ..Default::default()
    }))
    .unwrap();
    // Run the sampler matrix
    assert!(my_matrix.run().is_ok());
    // Get all deactivated features
    let deactivated = my_matrix
        .get_features(Some(&ScfsMatrix::any_inactive))
        .unwrap();
    // Confirm we have them
    assert_ne!(deactivated.len(), 0);
    // Setup test validator and logging while deactivating all
    // features that are deactivated in devnet
    let (test_validator, main_payer) = clean_ledger_setup_validator(deactivated).unwrap();
    let connection = test_validator.get_rpc_client();
    solana_logger::setup_with_default("solana_runtime::message=debug");

    let accounts = &[];
    let txn = submit_transaction(
        &connection,
        &main_payer,
        [
            // Add two (2) instructions to transaction
            // Replace with instructions that make sense for your program
            Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
            Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
        ]
        .to_vec(),
    );
    assert!(txn.is_ok());
}

Happy Testing!

その他参考資料

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: PokoPoko2ry