Pengujian Paritas Fitur

Saat menguji program Anda, jaminan bahwa program akan berjalan sama di berbagai cluster sangatlah penting untuk kualitas dan menghasilkan hasil yang diharapkan.

Fakta

Lembar Fakta

  • Fitur adalah kemampuan yang diperkenalkan ke validator Solana dan memerlukan aktivasi untuk digunakan.
  • Fitur dapat diaktifkan di satu cluster (misalnya testnet) tetapi tidak di cluster lain (misalnya mainnet-beta).
  • Namun; saat menjalankan solana-test-validator default secara lokal, semua fitur yang tersedia di Solana versi Anda diaktifkan secara otomatis. Hasilnya adalah ketika menguji secara lokal, kemampuan dan hasil dari pengujian Anda mungkin tidak sama saat menerapkan dan menjalankan di cluster yang berbeda!

Skenario

Asumsikan Anda memiliki Transaksi yang berisi tiga (3) instruksi dan setiap instruksi mengkonsumsi kira-kira 100_000 Compute Unit (CU). Saat menjalankan dalam versi Solana 1.8.x, Anda akan mengamati konsumsi CU instruksi Anda mirip dengan:

InstruksiMulai CUEksekusiSisa CU
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_000

Di Solana 1.9.2 fitur yang disebut 'transaction wide compute cap' diperkenalkan di mana Transaksi, secara default, memiliki anggaran 200_000 CU dan instruksi yang dienkapsulasi draw down dari anggaran Transaksi tersebut. Menjalankan transaksi yang sama seperti yang sebelumnya akan menunjukkan hasil yang sangat berbeda:

InstruksiMulai CUEksekusiSisa CU
1200_000-100_000100_000
2100_000-100_0000
30GAGAL!!!GAGAL!!!

Astaga! Jika Anda tidak menyadari hal ini, Anda mungkin akan frustrasi karena tidak ada perubahan pada behavior instruksi Anda yang akan menyebabkan ini. Di devnet itu berfungsi dengan baik, tetapi secara lokal gagal?!?

Ada kemampuan untuk meningkatkan keseluruhan anggaran Transaksi, katakanlah 300_000 CU, dan selamatkan kewarasan Anda, namun ini menunjukkan mengapa pengujian dengan Feature Parity memberikan cara proaktif untuk menghindari kebingungan.

Status Fitur

Cukup mudah untuk memeriksa fitur apa yang diaktifkan untuk cluster tertentu dengan perintah 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

Atau, Anda dapat menggunakan alat seperti scfsd untuk mengamati status semua fitur di seluruh cluster yang akan ditampilkan, sebagian layar ditampilkan di sini, dan tidak memerlukan solana-test-validator untuk dijalankan:

Heatmap dari Status FiturFeature Status Heatmap

Uji Paritas

Seperti disebutkan di atas, solana-test-validator mengaktifkan semua fitur secara otomatis. Jadi untuk menjawab pertanyaan "Bagaimana saya bisa menguji secara lokal di lingkungan yang memiliki paritas dengan devnet, testnet atau bahkan mainnet-beta?".

Solusi: PR ditambahkan ke Solana 1.9.6 untuk memungkinkan penonaktifan fitur:

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

Demonstrasi Sederhana

Misalkan Anda memiliki program sederhana yang mencatat data yang diterimanya di entry-point. Dan Anda menguji transaksi yang menambahkan dua (2) instruksi untuk program Anda.

Semua fitur diaktifkan

  1. Anda memulai validator uji di satu terminal:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program target/deploy/PROGNAME.so --reset`
  1. Di terminal lain Anda memulai log streamer:
solana logs
  1. Anda kemudian menjalankan transaksi Anda. Anda akan melihat sesuatu yang serupa di terminal log (diedit untuk kejelasan):
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[

Karena fitur 'batas komputasi luas transaksi' kami secara otomatis diaktifkan secara default, kami mengamati masing-masing instruksi penarikan CU dari anggaran awal Transaksi sebesar 200_000 CU.

Fitur selektif dinonaktifkan

  1. Untuk menjalankan ini, kami ingin menjalankan agar behavior anggaran CU setara dengan apa yang berjalan di devnet. Menggunakan alat yang dijelaskan dalam Status Fitur pun kami mengisolasi kunci publik batasan komputasi luas transaksi dan gunakan --deactivate-feature pada uji startup validator
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. Kami sekarang melihat di log kami bahwa instruksi kami sekarang memiliki anggaran 200_000 CU sendiri (diedit untuk kejelasan) yang saat ini merupakan status di semua cluster hulu:
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

Uji Paritas Penuh

Anda dapat berada dalam paritas penuh dengan cluster tertentu dengan mengidentifikasi setiap fitur yang belum diaktifkan dan tambahkan --deactivate-feature <FEATURE_PUBKEY> untuk masing-masing saat menjalankan solana-test-validator:

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

Atau, scfsd menyediakan sakelar perintah untuk menampilkan fitur yang dinonaktifkan sepenuhnya setel agar kluster diumpankan langsung ke startup solana-test-validator:

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

Jika Anda membuka terminal lain, saat validator sedang berjalan, dan status fitur solana Anda akan melihat fitur dinonaktifkan yang ditemukan dinonaktifkan di devnet

Uji Paritas Penuh Secara Terprogram

Bagi mereka yang mengontrol jalannya validator pengujian dalam kode pengujian mereka, memodifikasi fitur validator uji yang diaktifkan/dinonaktifkan dimungkinkan menggunakan TestValidatorGenesis. Dengan Solana 1.9.6 sebuah fungsi yang telah ditambahkan ke pembuat validator untuk mendukung ini.

Di root folder program Anda, buat folder baru bernama tests dan tambahkan parity_test.rs mengajukan. Berikut adalah fungsi tempat boiler (boiler-plate jika Anda mau) yang digunakan oleh setiap pengujian

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
}

Kita sekarang dapat menambahkan fungsi pengujian di badan mod test {...} untuk mendemonstrasikan default penyiapan validator (semua fitur diaktifkan) lalu nonaktifkan batas komputasi luas transaksi sebagai per contoh sebelumnya menjalankan solana-test-validator dari CLI.

We can now add test functions in the body of mod test {...} to demonstrate default validator setup (all features enabled) and then disabling the transaction wide compute cap as per previous examples running solana-test-validator from the command line.

#[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());
}

Atau, gadget mesin scfs dapat menghasilkan vektor penuh yang dinonaktifkan fitur untuk sebuah cluster. Berikut ini menunjukkan menggunakan mesin itu untuk mendapatkan daftar dari semua fitur yang dinonaktifkan untuk 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());
}

Selamat menguji!

Resources

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: akangaziz