Prueba de paridad de características

Al probar su programa, la garantía de que se ejecutará de la misma manera en varios clusters es esencial tanto para la calidad como para producir los resultados esperados.

Hechos

Hoja de hechos

  • Las características son capacidades que se introducen en los validadores de Solana y requieren activación para ser utilizadas.
  • Las características pueden activarse en un clúster (p. ej., testnet) pero no en otro (p. ej., mainnet-beta).
  • Sin embargo; al ejecutar solana-test-validator localmente, todas las características disponibles se activan automáticamente. El resultado es que cuando se prueba localmente, las capacidades y los resultados puede que no sean las mismas que al ejecutar en un clúster diferente!

Escenario

Suponga que tiene una Transacción que contenía tres (3) instrucciones y cada instrucción consume aproximadamente 100_000 Unidades de cómputo (CU). Cuando se ejecuta en una versión de Solana 1.8.x, observaría un consumo de CU de instrucción similar a:

InstructionInicio de CUEjecuciónCU Restante
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_000

En Solana 1.9.2, se introdujo una función llamada 'límite de cómputo amplio de transacción' donde una transacción, de forma predeterminada, tiene un presupuesto de 200_000 CU y las instrucciones encapsuladas draw down de ese presupuesto de transacción. Corriendo la misma transacción como se señaló anteriormente tendría un comportamiento muy diferente:

InstructionInicio de CUEjecuciónCU Restante
1200_000-100_000100_000
2100_000-100_0000
30FALLA!!!FALLA!!!

¡Ay! Si no estuviera al tanto de esto, probablemente se sentiría frustrado ya que no hubo cambios en su comportamiento de instrucción que causaría esto. En devnet funcionó bien, pero localmente estaba fallando?!?

Existe la posibilidad de aumentar el presupuesto general de transacciones, digamos 300_000 CU, y que no falle pero esto demuestra por qué probar con Feature Parity proporciona una forma proactiva de evitar confusiones.

Estado de la función

Es bastante fácil verificar qué funciones están habilitadas para un clúster en particular con el comando 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

Alternativamente, puede usar una herramienta como scfsd para observar el estado de todas las funciones en los clústeres, que mostraría la pantalla parcial que se muestra aquí, y no requiere que solana-test-validator se esté ejecutando:

Feature Status Heatmap

Pruebas de paridad

Como se señaló anteriormente, solana-test-validator activa todas las características automáticamente. Entonces, para responder a la pregunta "¿Cómo puedo probar localmente en un entorno que tiene paridad con devnet, testnet o incluso mainnet-beta?".

Solución: PRs fueron agregados a Solana 1.9.6 para permitir la desactivación de funciones:

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

Demostración sencilla

Suponga que tiene un programa simple que registra los datos. Y usted esta probando una transacción que agrega dos (2) instrucciones para su programa.

Todas las funciones activadas

  1. Inicie el validador de prueba en una terminal:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program target/deploy/PROGNAME.so --reset`
  1. En otra terminal, inicia el transmisor de registros:
solana logs
  1. Luego ejecuta su transacción. Vería algo similar en el terminal de registro (editado para mayor claridad):
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[

Debido a que nuestra característica 'límite de cómputo de toda la transacción' se activa automáticamente de forma predeterminada, observamos cada instrucción que reduce CU del presupuesto de transacción inicial de 200_000 CU.

Funciones selectivas desactivadas

  1. Para esta ejecución, queremos que el comportamiento del presupuesto de CU esté a la par con lo que se ejecuta en devnet. Usando la(s) herramienta(s) descrita(s) en Estado de la características aislamos la clave pública transaction wide computing cap y usamos --deactivate-feature en el inicio del validador de prueba
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. Ahora vemos en nuestros registros que nuestras instrucciones ahora tienen su propio presupuesto de 200_000 CU (editado para mayor claridad) que es actualmente el estado en todos los 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

Pruebas de paridad completa

Puede estar en paridad total con un clúster específico identificando cada función que no está aún activada y agregue --deactivate-feature <FEATURE_PUBKEY> para cada uno al invocar solana-test-validator:

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

Alternativamente, scfsd proporciona un interruptor que genera una salida para la función desactivada para un clúster para que se use directamente al inicio de solana-test-validator:

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

Si abre otra terminal, mientras el validador se está ejecutando, y ejecuta solana feature status verá características desactivadas que se encontraron desactivadas en devnet

Pruebas de paridad completa programáticamente

Para aquellos que controlan la ejecución del validador de prueba dentro de su código de prueba, modificando las características activadas/desactivadas del validador de prueba es posible usando TestValidatorGenesis. Con Solana 1.9.6 se ha agregado una función al generador de validadores para admitir esto.

En la raíz de la carpeta de su programa, cree una nueva carpeta llamada tests y agregue un archivo llamado parity_test.rs. Aquí estarán las funciones utilizadas por cada prueba.

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
}

Ahora podemos agregar funciones de prueba en el cuerpo de mod test {...} para demostrar el valor predeterminado de configuración del validador (todas las funciones habilitadas) y luego deshabilitando el "límite de cómputo de toda la transacción" como los ejemplos anteriores ejecutando solana-test-validator desde la línea 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, el gadget del motor scfs puede producir un vector completo de desactivado de características de un clúster. Lo siguiente demuestra el uso de ese motor para obtener una lista de todas las funciones desactivadas para 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());
}

Happy Testing!

Recursos

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: Marco Ordonez