Feature Parity Testing

당신이 프로그램을 테스트할 때, 다양한 cluster들에서 동일한 것을 실행할 것이라는 확신은 프로그램의 질과 예측된 결과를 만들기 위해 매우 필수적입니다.

Facts

Fact Sheet

  • 기능들은 Solana validator들에게 소개되는 기능들로 사용하기 위해 활성화가 필요합니다.
  • 기능들은 하나의 cluster(e.g. testnet) 에서 활성화될지 모르지만 다른 cluster(e.g. mainnet-beta) 에서는 아닐 수 있습니다.
  • 그러나 로컬에서 기본 solana-test-validator를 실행할 때, 당신의 solana version에서 가능한 모든 기능들이 자동으로 활성화될 것입니다. 로컬에서 테스트할때 테스트 결과들은 다른 cluster에 배포하고 실행한 결과와 다를 수도 있습니다.

Scenario

당신이 3개의 Instuction들을 포함하는 하나의 Transaction을 갖고 각 Instruction은 대략 100_000 컴퓨터 유닛을 소비한다고 가정합시다. Solana 1.8.x 버전에서 돌릴 때, 당신은 아래와 유사한 Instruction CU 소비를 보게 될 것입니다.

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

솔라나 1.9.2 버전에서 'transaction wide comput cap'이라 불리는 기능이 소개되었는데, 여기서 Transaction은 기본적으로 200_000 CU 에산을 갖고 캡슐화된 Instruction들은 이 Transaction 예산에서 끌어와 사용됩니다. 위에서 언급된 같은 Transaction을 실행하면 다른 결과를 얻게 될 것입니다:

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

당신이 이것을 알지 못했다면, 당신의 Instruction은 변화가 없음에도 이것을 발생시키는 것을 보고 좌절할지도 모릅니다. devnet에서는 괘찮지만 local에서는 실패?!?

당신의 정신을 지키기 위해 300_000 CU를 허락하도록 전체적인 Transaction budget을 증가시키는 능력이 존재합니다. 그러나 이 글은 **Feature Parity**를 가지고 테스트하는 것이 혼란을 피하기 위한 대책을 제공하는 이유임을 입증하는 것입니다.

Feature Status

solana feature status command롤 특정 cluster에서 가능한 기능들이 무엇이 있는 지 쉽게 확인할 수 있습니다.

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

대안으로, 당신은 cluster들 사이에 모든 기능 상태를 확인하기 위해 scfsd 같은 도구를 사용할 수도 있습니다. 이것은 여기서는 이부분이지만 출력해주고, solana-test-validator 실행을 요구하지 않습니다.

Feature Status Heatmap

Parity Testing

위에서 언급했듯이, solana-test-validator모든 기능들을 자동으로 활성화합니다. 그래서, "devnet, testnet, mainnet-beta 같은 환경을 로컬에서는 어떻게 테스트할까요?"라는 질문에 대답하기 위해서.

해결책: 기능들을 비활성화 가능하게 하는 PR들이 솔라나 1.9.6 버전에 추가됐습니다:

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

Simple Demonstration

Suppose you have a simple program that logs the data it receives in it's entry-point. And you are testing a transaction that adds two (2) instructions for your program. 당신이 entry-point에서 수신된 데이터를 출력하는 간단한 프로그램을 가지고 있다고 가정합시다. 그리고 당신은 프로그램에 2 개의 Intrustion을 추가하는 Transaction을 테스트하고 있습니다.

All features activated

  1. 당신은 하나의 터미널에서 test validator를 실행합니다.
solana config set -ul
solana-test-validator -l ./ledger --bpf-program target/deploy/PROGNAME.so --reset`
  1. 다른 하나의 터미널에서 당신은 log streamer를 실행합니다.
solana logs
  1. 그리고 당신은 Transaction을 실행합니다. 당신은 log terminal에서 아래와 비슷한 것을 보게 될 것입니다 (명확성을 위해 일부 수정된):
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 comput cap'이 자동으로 활성화되어 있기 때문에, 우리는 첫 Transaction의 200_000 CU부터 다운되는 각 Instruction을 볼 수 있습니다.

Selective features deactivated

  1. 우리는 CU budget 행위가 devnet에 있는 것처럼 실행하고 싶습니다. Feature Status에 설명된 도구를 사용해서 transaction wide compute cap public key를 고립시키고 test validator를 시작할 떄 transaction wide compute cap 옵션을 사용할 것입니다.
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. 우리는 이제 Instruction들이 각자의 200_000 budget을 갖고 있는 것을 볼 수 있습니다. 이것은 모든 upstream cluster들의 현재 상태입니다.
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

Full Parity Testing

당신은 solana-test-validator를 실행할 때 --deactivate-feature <FEATURE_PUBKEY>를 추가하고 아직 활성화되지 않은 가 기능들을 식별하면서 특정 cluster와 완전히 같게 할 수 있습니다.

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

대안으로, scfsd는 한 클러스터를 위한 완전히 비활성화된 기능들로 스위치하는 command를 제공합니다. 이것은 solana-test-validator를 시작할 때 파라미터로 줍니다.

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

만약 당신이 validator가 실행 중일 때 또 다른 터미널을 열고 solana feature status를 실행하면, 당신은 devnet에서 비활성화된 기능들을 볼 것입니다.

Full Parity Testing Programmatically

테스트 코드 내에서 test validator 실행을 다루는 사람들을 위해, TestValidatorGenesis를 사용해서 test validator의 activated/deactivated 기능을 수정하는 것이 가능합니다. Solana 1.9.6에서 이것을 지원하기 위한 function이 validator에 추가됐습니다.

프로그램의 루트 폴더에서 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 {...}의 바디에 모든 기능이 활성화된 기본 validator 세팅과 이전 예제에서 command line에서 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은 특정 cluster에 비활성화된 모든 기능들의 전체 벡터를 만들 수 있습니다. 아래는 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!

Resources

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: Steven Luscher, TaeGit