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çãoCU InicialExecuçãoCU Restante
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_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çãoCU InicialExecuçãoCU Restante
1200_000-100_000100_000
2100_000-100_0000
30FALHA!!!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:

Feature Status Heatmap

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

  1. 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`
  1. Em outro terminal, você inicia o registro de logs:
solana logs
  1. 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

  1. 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`
  1. 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.

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
}

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!

Recursos

scfsdopen in new window

Mecanismo scfsopen in new window

Last Updated:
Contributors: Daniel Cukier