Teste de Paridade de Recursos
Ao testar seu programa, garantir que ele funcionará da mesma maneira em vários clusters é essencial tanto para a qualidade quanto para a obtenção de resultados esperados.
Fatos
Ficha Informativa
- Os recursos são capacidades que são introduzidas aos validadores da Solana e exigem ativação para serem usadas.
- Os recursos podem ser ativados em um cluster (por exemplo, testnet) mas não em outro (por exemplo, mainnet-beta).
- No entanto, ao executar o solana-test-validator padrão localmente, todos os recursos disponíveis em sua versão da Solana são ativados automaticamente. O resultado é que, ao testar localmente, as capacidades e resultados dos seus testes podem não ser os mesmos ao implantar e executar em um cluster diferente!
Cenário
Suponha que você tenha uma Transação que contém três (3) instruções e cada instrução consome aproximadamente 100.000 Unidades de Computação (CU). Ao executar na versão Solana 1.8.x, você observaria que o consumo de CU de suas instruções é semelhante ao seguinte:
Instrução | CU Inicial | Execução | CU Restante |
---|---|---|---|
1 | 200_000 | -100_000 | 100_000 |
2 | 200_000 | -100_000 | 100_000 |
3 | 200_000 | -100_000 | 100_000 |
Na versão da Solana 1.9.2, foi introduzido um recurso chamado 'transaction wide compute cap' (recurso de computação ampla de transação), em que uma Transação, por padrão, tem um orçamento de 200.000 CU e as instruções encapsuladas são deduzidas desse orçamento de Transação. Executar a mesma transação como observado acima teria um comportamento muito diferente:
Instrução | CU Inicial | Execução | CU Restante |
---|---|---|---|
1 | 200_000 | -100_000 | 100_000 |
2 | 100_000 | -100_000 | 0 |
3 | 0 | FALHA!!! | FALHA!!! |
Uau! Se você não estivesse ciente disso, provavelmente ficaria frustrado, já que não houve mudança no comportamento da sua instrução que pudesse causar isso. Na devnet funcionou bem, mas localmente falhou?!?
Existe a possibilidade de aumentar o orçamento total da Transação, para digamos 300.000 CU, e recuperar sua sanidade, mas isso demonstra por que testar com a Paridade de Recursos fornece uma maneira proativa de evitar qualquer confusão.
Status do Recurso
É bem fácil verificar quais recursos estão habilitados para um cluster específico com o comando solana feature status
.
solana feature status -ud // Exibe por status de recurso para a devnet
solana feature status -ut // Exibe para a testnet
solana feature status -um // Exibe para a mainnet-beta
solana feature status -ul // Exibe para a rede local, requer a execução do solana-test-validator
Alternativamente, você pode usar uma ferramenta como o scfsd para observar todos os estados dos recursos em todos os clusters, o que exibiria uma parte da tela mostrada aqui e não requer que o solana-test-validator
esteja em execução:
Teste de Paridade
Conforme observado anteriormente, o solana-test-validator
ativa automaticamente todos os recursos. Então, para responder à pergunta "como posso testar localmente em um ambiente que tenha paridade com a devnet, a testnet ou até mesmo a mainnet-beta?".
Solução: Foram adicionados PRs à versão da Solana 1.9.6 para permitir a desativação dos recursos:
solana-test-validator --deactivate-feature <FEATURE_PUBKEY> ...
Demonstração Simples
Suponha que você tenha um programa simples que registra os dados que recebe em seu ponto de entrada. E você está testando uma transação que adiciona duas (2) instruções ao seu programa.
Todos os recursos ativados
- Você inicia o validador de teste em um terminal:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program ADDRESS target/deploy/PROGNAME.so --reset`
- Em outro terminal, você inicia o registro de logs:
solana logs
- Em seguida, você executa sua transação. Você veria algo semelhante no terminal de registro (editado para maior clareza):
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[
Como nosso recurso 'transaction wide compute cap' é ativado automaticamente por padrão, observamos que cada instrução consome CUs do orçamento inicial da transação de 200.000 CU.
Recursos seletivos desativados
- Para esta execução, queremos executar para que o comportamento do orçamento de CU esteja em paridade com o que está sendo executado na devnet. Usando as ferramentas descritas no Status do Recurso, isolamos a chave pública do
transaction wide compute cap
e usamos a opção--deactivate-feature
na inicialização do validador de teste.
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
- Agora podemos ver em nossos logs que as instruções têm agora seu próprio orçamento de 200.000 CU (editado para maior clareza), que é o estado atual em todos os clusters upstream:
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
Teste de Paridade Total
Você pode estar em total paridade com um cluster específico identificando cada recurso que ainda não está ativado e adicionando um --deactivate-feature <FEATURE_PUBKEY>
para cada um ao invocar o solana-test-validator
:
solana-test-validator --deactivate-feature PUBKEY_1 --deactivate-feature PUBKEY_2 ...
Alternativamente, o scfsd fornece uma opção de comando para gerar uma saída do conjunto completo de recursos desativados para um cluster, para ser alimentado diretamente na inicialização do solana-test-validator
:
solana-test-validator -l ./.ledger $(scfsd -c devnet -k -t)
Se você abrir outro terminal, enquanto o validador está em execução, e executar o comando solana feature status
, você verá recursos desativados que foram encontrados desativados na devnet.
Teste de Paridade Total Programática
Para aqueles que controlam a execução do validador de teste em seu código de teste, é possível modificar os recursos ativados/desativados do validador de teste usando o TestValidatorGenesis
. Com a versão da Solana 1.9.6, uma função foi adicionada ao construtor do validador para suportar isso.
Na raiz da pasta do seu programa, crie uma nova pasta chamada tests
e adicione um arquivo parity_test.rs
. Aqui estão as funções de boilerplate usadas por cada teste.
#[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)
}
Agora podemos adicionar funções de teste no corpo de mod test {...}
para demonstrar a configuração padrão do validador (todos os recursos ativados) e, em seguida, desativar o transaction wide compute cap
, conforme exemplos anteriores de execução do solana-test-validator
a partir da linha de comando.
#[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());
}
Alternativamente, o mecanismo SCFS pode produzir um vetor completo de recursos desativados para um cluster. O seguinte demonstra o uso desse mecanismo para obter uma lista de todos os recursos desativados para a 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());
}
Feliz codificação!