light-client 0.25.0

Client library for Light Protocol
Documentation
//! Transaction v1 (SIMD-0385) tests against LiteSVM.
//!
//! Requires the `program-test` feature, which pulls in litesvm.
#![cfg(feature = "program-test")]

use agave_feature_set::enable_tx_v1;
use light_client::rpc::{
    build_v1_transaction, default_tx_config, with_defaults, RpcError, TransactionConfig,
    DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, MAX_LEGACY_TX_SIZE,
    MAX_TX_V1_SIZE,
};
use litesvm::LiteSVM;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_message::VersionedMessage;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::{versioned::VersionedTransaction, Transaction};

/// Above the rent-exempt minimum for a zero-data account, so recipients survive.
const TRANSFER_LAMPORTS: u64 = 1_000_000;

/// LiteSVM with the `enable_tx_v1` feature gate active.
///
/// LiteSVM itself does not enforce the gate (a validator rejects v1
/// transactions before they reach the SVM), so this mirrors the cluster state
/// the client will see rather than being required for execution.
fn svm_with_tx_v1() -> LiteSVM {
    let mut feature_set = LiteSVM::mainnet_feature_set();
    feature_set.activate(&enable_tx_v1::id(), 0);
    LiteSVM::new().with_feature_set(feature_set)
}

/// `count` transfers from `payer` to fresh recipients. Each adds a 32-byte
/// static key plus instruction overhead, so enough of them exceed 1,232 bytes.
fn transfer_instructions(payer: &Pubkey, count: usize) -> (Vec<Instruction>, Vec<Pubkey>) {
    let recipients: Vec<Pubkey> = (0..count).map(|_| Pubkey::new_unique()).collect();
    let instructions = recipients
        .iter()
        .map(|to| solana_system_interface::instruction::transfer(payer, to, TRANSFER_LAMPORTS))
        .collect();
    (instructions, recipients)
}

fn serialized_len(transaction: &VersionedTransaction) -> usize {
    bincode::serialize(transaction)
        .expect("serialize versioned transaction")
        .len()
}

#[test]
fn v1_transaction_above_legacy_limit_executes() {
    let mut svm = svm_with_tx_v1();
    let payer = Keypair::new();
    svm.airdrop(&payer.pubkey(), 1_000_000_000)
        .expect("airdrop");

    // 30 transfers: about 30 * 32 bytes of keys plus 30 * ~15 bytes of
    // compiled instructions, well past the legacy limit.
    let (instructions, recipients) = transfer_instructions(&payer.pubkey(), 30);
    let blockhash = svm.latest_blockhash();

    let legacy_len = bincode::serialize(&Transaction::new_signed_with_payer(
        &instructions,
        Some(&payer.pubkey()),
        &[&payer],
        blockhash,
    ))
    .expect("serialize legacy transaction")
    .len();
    assert!(
        legacy_len > MAX_LEGACY_TX_SIZE,
        "test instructions must not fit a legacy transaction, got {legacy_len} bytes"
    );

    let transaction = build_v1_transaction(
        &instructions,
        &payer.pubkey(),
        &[&payer],
        blockhash,
        TransactionConfig::default(),
    )
    .expect("build v1 transaction");

    let size = serialized_len(&transaction);
    assert!(
        size > MAX_LEGACY_TX_SIZE && size <= MAX_TX_V1_SIZE,
        "v1 transaction size {size} not in ({MAX_LEGACY_TX_SIZE}, {MAX_TX_V1_SIZE}]"
    );

    let result = svm.send_transaction(transaction);
    assert!(result.is_ok(), "v1 transaction failed: {:?}", result.err());

    let balances: Vec<u64> = recipients
        .iter()
        .map(|to| svm.get_account(to).map(|a| a.lamports).unwrap_or(0))
        .collect();
    assert_eq!(balances, vec![TRANSFER_LAMPORTS; recipients.len()]);
}

#[test]
fn v1_transaction_carries_config_in_message() {
    let payer = Keypair::new();
    let (instructions, _) = transfer_instructions(&payer.pubkey(), 1);
    let config = TransactionConfig {
        priority_fee: Some(5_000),
        compute_unit_limit: Some(200_000),
        loaded_accounts_data_size_limit: Some(1_000_000),
        heap_size: None,
    };

    let transaction = build_v1_transaction(
        &instructions,
        &payer.pubkey(),
        &[&payer],
        LiteSVM::new().latest_blockhash(),
        config,
    )
    .expect("build v1 transaction");

    let message = match &transaction.message {
        VersionedMessage::V1(message) => message,
        other => panic!("expected a v1 message, got {other:?}"),
    };
    assert_eq!(message.config, config);
    assert_eq!(
        transaction.version(),
        solana_transaction::versioned::TransactionVersion::Number(1)
    );
    // No ComputeBudget instructions are added to a v1 message.
    assert_eq!(message.instructions.len(), instructions.len());
}

#[test]
fn v1_defaults_fill_zero_limits() {
    let filled = with_defaults(TransactionConfig::default());
    assert_eq!(
        filled,
        TransactionConfig {
            priority_fee: None,
            compute_unit_limit: Some(DEFAULT_COMPUTE_UNIT_LIMIT),
            loaded_accounts_data_size_limit: Some(DEFAULT_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
            heap_size: None,
        }
    );
    assert_eq!(default_tx_config(), filled);

    let explicit = TransactionConfig {
        priority_fee: Some(1),
        compute_unit_limit: Some(2),
        loaded_accounts_data_size_limit: Some(3),
        heap_size: Some(64 * 1024),
    };
    assert_eq!(with_defaults(explicit), explicit);
}

#[test]
fn v1_build_fails_without_payer_signature() {
    let payer = Keypair::new();
    let other = Keypair::new();
    let (instructions, _) = transfer_instructions(&payer.pubkey(), 1);

    let err = build_v1_transaction(
        &instructions,
        &payer.pubkey(),
        &[&other],
        LiteSVM::new().latest_blockhash(),
        TransactionConfig::default(),
    )
    .expect_err("signing with the wrong key must fail");
    assert!(matches!(err, RpcError::SigningError(_)), "{err:?}");
}