Test de Conformité des Fonctionnalités

Lorsque vous testez votre programme, il est essentiel de s'assurer qu'il fonctionnera de la même manière dans différents clusters, tant pour la qualité que pour l'obtention des résultats attendus.

Faits

Fiche d'Information

  • Les fonctionnalités sont des changements qui sont introduits dans le code des validateurs Solana et qui nécessitent une activation pour être utilisés.
  • Les fonctionnalités peuvent être activées dans un cluster (par exemple testnet) mais pas dans un autre (par exemple mainnet-beta).
  • Cependant, lorsque vous exécutez localement la version par défaut de solana-test-validator, toutes les fonctionnalités disponibles dans votre version de Solana sont automatiquement activées. Le résultat est que lorsque vous testez localement, les fonctionnalités et les résultats de vos tests peuvent ne pas être les mêmes lorsque vous déployez et exécutez dans un cluster différent !

Scénario

Supposons que vous ayez une Transaction qui contient trois (3) instructions et que chaque instruction consomme environ 100_000 Unités de Calcul (UC). Lors de l'exécution sur une version Solana 1.8.x, vous observeriez une consommation de CU d'instruction ressemblant à :

InstructionCU de départExécutionCU Restants
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_000

Dans Solana 1.9.2, une fonctionnalité appelée "plafond de calcul pour l'ensemble des transactions (transaction wide compute cap)" a été introduite. Par défaut, une Transaction a un budget de 200_000 CU et les instructions qui la composent vont débiter ce budget de Transaction. L'exécution de la même transaction indiquée ci-dessus aurait un comportement très différent :

InstructionCU de départExécutionCU Restants
1200_000-100_000100_000
2100_000-100_0000
30FAIL!!!FAIL!!!

Aïe ! Si vous n'en étiez pas conscient, vous seriez probablement frustré car aucun changement dans vos instructions n'aurait pu provoquer ce phénomène. Sur le devnet il fonctionne bien, mais localement il échoue ?!?

Il est possible d'augmenter le budget global de la Transaction, par exemple à 300_000 UC, et de sauver votre santé mentale, mais cela montre pourquoi les tests avec _Conformité des Fonctionnalités constituent un bon moyen d'éviter toute confusion.

Statut de la Fonctionnalité

Il est assez facile de vérifier quelles fonctionnalités sont disponibles pour un cluster donné avec la commande 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

Alternativement, vous pouvez utiliser un outil comme scfsd pour observer le statut de toutes les fonctionnalités des différents clusters qui afficherait l'écran partiel montré ici et qui ne nécessite pas l'exécution de solana-test-validator :

Feature Status Heatmap

Test de Conformité

Comme indiqué ci-dessus, solana-test-validator active toutes les fonctionnalités automatiquement. Donc, pour répondre à la question "Comment puis-je tester localement dans un environnement qui a une conformité avec le devnet, le testnet ou encore le mainnet-beta ?".

Solution: Des PRs ont été ajoutés à Solana 1.9.6 pour permettre la désactivation des fonctionnalités :

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

Démonstration Simple

Supposons que vous ayez un programme simple qui enregistre les données qu'il reçoit en entrée et vous testez une transaction qui ajoute deux (2) instructions à votre programme.

Toutes les fonctionnalités activées

  1. Vous lancez le validateur de test dans un terminal :
solana config set -ul
solana-test-validator -l ./ledger --bpf-program target/deploy/PROGNAME.so --reset`
  1. Dans un autre terminal, vous démarrez le flux de logs :
solana logs
  1. Vous exécutez ensuite votre transaction. Vous verriez quelque chose de similaire dans le terminal de log (édité pour plus de clarté) :
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[

Comme notre fonctionnalité "plafond de calcul pour l'ensemble des transactions" est automatiquement activée par défaut, nous observons que chaque instruction prélève des UC sur le budget de transaction de départ de 200_000 UC.

Fonctionnalités sélectives désactivées

  1. Pour cette exécution, nous voulons faire en sorte que le comportement du budget de CU soit en conformité avec ce qui est exécuté sur le devnet. En utilisant le(s) outil(s) décrit(s) dans Statut de la Fonctionnalité nous isolons la clé publique transaction wide compute cap et utilisons la fonction --deactivate-feature au démarrage du validateur de test
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. Nous voyons maintenant dans nos logs que nos instructions ont maintenant leur propre budget de 200_000 CU (édité pour plus de clarté) qui est actuellement l'état dans tous les clusters :
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

Tests de Conformité Complète

Vous pouvez être en conformité complète avec un cluster donné en identifiant chaque fonctionnalité qui n'est pas encore activée et ajouter un --deactivate-feature <FEATURE_PUBKEY> pour chacune d'entre elles lors de l'exécution de solana-test-validator:

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

Alternativement, scfsd fournit un switch de commande pour retourner l'ensemble complet des fonctionnalités désactivées pour un cluster afin d'alimenter le démarrage de solana-test-validator :

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

Si vous ouvrez un autre terminal, alors que le validateur est en cours d'exécution, et que vous tapez solana feature status, vous verrez les fonctionnalités désactivées qui ont été trouvées désactivées sur le devnet

Test de Conformité Complète programmé

Pour ceux qui contrôlent l'exécution du validateur de test dans leur code de test, il est possible de modifier les fonctions d'activation/désactivation du validateur de test en utilisant TestValidatorGenesis. Avec Solana 1.9.6, une fonction a été ajoutée au constructeur de validateurs pour prendre en charge cette fonction.

A la racine du dossier de votre programme, créez un nouveau dossier appelé tests et ajoutez un fichier parity_test.rs. Voici les fonctions de base utilisées pour chaque test

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
}

Nous pouvons maintenant ajouter des fonctions de test dans le corps de mod test {...} pour démontrer la configuration par défaut du validateur (toutes les fonctionnalités activées) et ensuite désactiver transaction wide compute cap acomme dans les exemples précédents en exécutant solana-test-validator à partir de la ligne de commande.

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

Alternativement, le gadget du moteur scfs (scfs engine gadget) peut produire un vecteur complet de fonctionnalités désactivées pour un cluster. L'exemple suivant démontre l'utilisation de ce moteur pour obtenir une liste de toutes les fonctionnalités désactivées sur le 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());
}

Bon test !

Ressources

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: cryptoloutre