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 à :
Instruction | CU de départ | Exécution | CU Restants |
---|---|---|---|
1 | 200_000 | -100_000 | 100_000 |
2 | 200_000 | -100_000 | 100_000 |
3 | 200_000 | -100_000 | 100_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 :
Instruction | CU de départ | Exécution | CU Restants |
---|---|---|---|
1 | 200_000 | -100_000 | 100_000 |
2 | 100_000 | -100_000 | 0 |
3 | 0 | FAIL!!! | 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
:
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
- 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`
- Dans un autre terminal, vous démarrez le flux de logs :
solana logs
- 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
- 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`
- 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
#[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
}
/// 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)
}
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 !