Funktionsparitätstest

Beim Testen Ihres Programms ist die Zusicherung erwartete Ergebnisse zu produzieren als auch in verschiedenen Clustern gleich ausgeführt wird, sowohl für die Qualität als auch für die Sicherheit von entscheidender Bedeutung.

Fakten

Fact Sheet

  • Funktionen sind Funktionen, die in Solana-Validatoren eingeführt werden und für deren Verwendung eine Aktivierung erforderlich ist.
  • Funktionen können in einem Cluster (z. B. Testnet) aktiviert werden, in einem anderen (z. B. Mainnet-Beta) jedoch nicht.
  • Jedoch; Wenn Sie standardmäßig solana-test-validator lokal ausführen, werden alle verfügbaren Funktionen in Ihrer Die Solana-Version wird automatisch aktiviert. Das Ergebnis ist, dass beim Testen vor Ort die Fähigkeiten und Ergebnisse von Ihre Tests sind möglicherweise nicht die gleichen, wenn Sie sie in einem anderen Cluster bereitstellen und ausführen!

Szenario

Angenommen, Sie haben eine Transaktion, die drei (3) Anweisungen enthält, und jede Anweisung verbraucht ungefähr 100_000 Recheneinheiten (CU). Wenn Sie eine Solana 1.8.x-Version ausführen, würden Sie Ihren Befehls-CU-Verbrauch ähnlich wie folgt beobachten:

InstructionStarting CUExecutionRemaining CU
1200_000-100_000100_000
2200_000-100_000100_000
3200_000-100_000100_000

In Solana 1.9.2 wurde eine Funktion namens „transaktionsweite Rechenobergrenze“ eingeführt, bei der eine Transaktion standardmäßig ein Budget von 200_000 CU hat und die Anweisungen draw down aus diesem Transaktionsbudget gekapselten sind. Die selbe Transaktion, wie oben erwähnt laufen zu lassen, würde ein ganz anderes Verhalten haben:

InstructionStarting CUExecutionRemaining CU
1200_000-100_000100_000
2100_000-100_0000
30FAIL!!!FAIL!!!

Huch! Wenn Sie sich dessen nicht bewusst wären, wären Sie wahrscheinlich frustriert, da sich Ihr Unterrichtsverhalten dadurch nicht geändert hat. Auf devnet hat es gut funktioniert, aber lokal hat es nicht funktioniert?!?

Es besteht die Möglichkeit, das gesamte Transaktionsbudget zu erhöhen, sagen wir auf 300_000 CU, und Ihre geistige Gesundheit zu retten. Dies zeigt jedoch, warum das Testen mit Feature Parity eine proaktive Möglichkeit bietet, Verwirrung zu vermeiden.

Feature Status

Mit dem Befehl „solana feature status“ lässt sich recht einfach überprüfen, welche Funktionen für einen bestimmten Cluster aktiviert sind.

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

Alternativ können Sie ein Tool wie scfsd verwenden, um den gesamten Funktionsstatus über Cluster hinweg zu beobachten was den hier gezeigten Teilbildschirm anzeigen würde und nicht erfordert, dass solana-test-validator läuft:

Feature Status Heatmap

Paritätstest

Wie oben erwähnt, aktiviert der solana-test-validator alle Features automatisch. Wie wird die Frage "Wie kann ich lokal in einer Umgebung testen, die Parität mit devnet hat, Testnet oder sogar Mainnet-Beta?" also beantwortet?

Lösung: PRs wurden zu Solana 1.9.6 hinzugefügt, um die Deaktivierung von Funktionen zu ermöglichen:

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

Einfache Vorführung

Angenommen, Sie haben ein einfaches Programm, das die empfangenen Daten an seinem Einstiegspunkt protokolliert. Und du testest eine Transaktion, die zwei (2) Anweisungen für Ihr Programm hinzufügt.

Alle Funktionen aktiviert

  1. Sie starten den Test Validator in einem Terminal:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program ADDRESS target/deploy/PROGNAME.so --reset`
  1. In einem anderen Terminal starten Sie den Log-Streamer:
solana logs
  1. Anschließend führen Sie Ihre Transaktion aus. Sie werden etwas Ähnliches im Protokollterminal sehen (aus Gründen der Übersichtlichkeit bearbeitet):
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[

Da unser Feature „transaktionsweites Compute Cap“ standardmäßig automatisch aktiviert ist, beachten wir das jeweils Anweisung, die CU aus dem anfänglichen Transaktionsbudget von 200_000 CU abzieht.

Selektive Funktionen deaktiviert

  1. Für diesen Lauf wollen wir so laufen, dass das CU-Budgetverhalten mit dem übereinstimmt, was in devnet läuft. Verwenden die in Funktionsstatus beschriebenen Tools isolieren wir den öffentlichen Schlüssel „transaktionsweite Compute-Obergrenze“. und verwenden Sie das --deactivate-feature beim Start des Testvalidators
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
  1. Wir sehen jetzt in unseren Protokollen, dass unsere Anweisungen jetzt ein eigenes Budget von 200_000 CU haben (aus Gründen der Klarheit bearbeitet). derzeit der Zustand in allen Upstream-Clustern:
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

Vollständiger Paritätstest

Sie können mit einem bestimmten Cluster in voller Parität sein, indem Sie jedes Feature identifizieren, das dies nicht ist noch aktiviert und fügen Sie jeweils ein --deactivate-feature <FEATURE_PUBKEY> hinzu, wenn Sie solana-test-validator aufrufen:

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

Alternativ stellt scfsd einen Befehlsschalter zur Verfügung, um das vollständig deaktivierte Feature auszugeben Set für einen Cluster, der direkt in den Start von "solana-test-validator" eingespeist wird:

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

Wenn Sie ein anderes Terminal öffnen, während der Validator läuft, und "solana feature status" sehen Sie deaktivierte Features, die im devnet deaktiviert gefunden wurden

Vollständiger Paritätstest programmgesteuert

Für diejenigen, die die Ausführung des Testvalidators in ihrem Testcode kontrollieren, Modifizieren Die aktivierten/deaktivierten Funktionen des Testvalidators sind mit TestValidatorGenesis möglich. Mit Solana 1.9.6 wurde dem Validator Builder eine Funktion hinzugefügt, um dies zu unterstützen.

Erstellen Sie im Stammverzeichnis Ihres Programmordners einen neuen Ordner mit dem Namen „tests“ und fügen Sie eine Datei „parity_test.rs“ hinzu Datei. Hier sind die Kesselplatzfunktionen (Kesselplatte, wenn Sie so wollen), die von jedem Test verwendet werden

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
}

Wir können jetzt Testfunktionen in den Hauptteil von mod test {...} einfügen, um die Standardeinstellung zu demonstrieren Validator-Setup (alle Funktionen aktiviert) und dann die „transaktionsweite Compute-Obergrenze“ deaktivieren wie in den vorherigen Beispielen, die solana-test-validator von der Befehlszeile aus ausführen.

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

Alternativ kann das scfs engine gadget einen vollständig deaktivierten Vektor von Funktionen für einen Cluster erzeugen. Im Folgenden wird die Verwendung dieser Engine zum Abrufen einer Liste aller deaktivierten Features für devnet veranschaulicht.

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

Fröhliches testen!

Ressourcen

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: nyk