Rust
Pinocchio pour les nuls

Pinocchio pour les nuls

Instructions par lots

Les invocations inter-programmes (CPI) entraînent un coût de base de 1 000 unités de calcul par appel. Pour les programmes qui reçoivent fréquemment des CPI au sein de la même instruction, cette surcharge devient un goulot d'étranglement significatif en termes de performance.

Pour résoudre cette inefficacité, Dean a créé l'instruction "batch" dans cette PR pour p-token, permettant plusieurs opérations en un seul appel CPI.

L'instruction par lots

Une instruction par lots permet de traiter plusieurs opérations en un seul CPI au lieu de nécessiter des appels séparés pour chaque opération. Cela réduit considérablement la consommation d'unités de calcul pour les programmes gérant plusieurs opérations liées.

Structure

L'instruction par lots utilise une structure d'en-tête améliorée contenant :

  • Nombre de comptes : Nombre de comptes requis pour l'instruction interne

  • Longueur des données : Taille des données d'instruction

Cet en-tête permet un traitement efficace de plusieurs instructions "internes" au sein d'un seul lot. Le système parcourt et traite chaque instruction interne séquentiellement.

Nous utilisons u8::MAX (255) comme discriminateur pour les instructions par lots.

Conception du point d'entrée

Le point d'entrée vérifie d'abord le discriminateur d'instruction pour déterminer s'il faut traiter un lot ou une instruction régulière. Cela empêche d'imbriquer des instructions par lots dans d'autres instructions par lots, ce qui serait incorrect.

Comme ceci :

rust
#[inline(always)]
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let [discriminator, remaining @ ..] = instruction_data else {
        return Err(TokenError::InvalidInstruction.into());
    };

    let result = if *discriminator == 255 {
        // 255 - Batch
        #[cfg(feature = "logging")]
        pinocchio::msg!("Instruction: Batch");

        process_batch(accounts, remaining)
    } else {
        inner_process_instruction(accounts, instruction_data)
    };

    result.inspect_err(log_error)
}

Traitement par lots

La fonction process_batch gère la logique principale de traitement par lots :

rust
/// The size of the batch instruction header.
///
/// The header of each instruction consists of two `u8` values:
///  * number of the accounts
///  * length of the instruction data
const IX_HEADER_SIZE: usize = 2;

pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult {
    loop {
        // Validates the instruction data and accounts offset.

        if instruction_data.len() < IX_HEADER_SIZE {
            // The instruction data must have at least two bytes.
            return Err(TokenError::InvalidInstruction.into());
        }

        // SAFETY: The instruction data is guaranteed to have at least two bytes
        // (header) + one byte (discriminator) and the values are within the bounds
        // of an `usize`.
        let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize };
        let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize };

        if instruction_data.len() < data_offset || data_offset == IX_HEADER_SIZE {
            return Err(TokenError::InvalidInstruction.into());
        }

        if accounts.len() < expected_accounts {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        // Process the instruction.

        // SAFETY: The instruction data and accounts lengths are already validated so
        // all slices are guaranteed to be valid.
        let (ix_accounts, ix_data) = unsafe {
            (
                accounts.get_unchecked(..expected_accounts),
                instruction_data.get_unchecked(IX_HEADER_SIZE..data_offset),
            )
        };

        inner_process_instruction(ix_accounts, ix_data)?;

        if data_offset == instruction_data.len() {
            // The batch is complete.
            break;
        }

        accounts = &accounts[expected_accounts..];
        instruction_data = &instruction_data[data_offset..];
    }

    Ok(())
}

Voici ce qui se passe dans cette fonction :

  • Validation de l'en-tête : La fonction commence par valider que les données d'instruction contiennent au moins la taille d'en-tête requise (2 octets).

  • Extraction des comptes et des données : Elle extrait le nombre de comptes attendu et calcule le décalage des données, en validant ces valeurs pour éviter un comportement indéfini.

  • Traitement des instructions : Chaque instruction interne est traitée à l'aide de la fonction standard inner_process_instruction avec ses comptes et données spécifiques.

  • Contrôle de boucle : La fonction continue le traitement jusqu'à ce que toutes les instructions par lots soient terminées, en avançant les pointeurs de compte et de données d'instruction pour chaque itération.

Certaines instructions peuvent nécessiter des vérifications explicites de propriété lorsqu'elles sont exécutées par lots, car l'environnement d'exécution ne vérifie la propriété qu'à la fin du traitement par lots. Les instructions qui ne modifient pas les comptes ou qui effectuent déjà des vérifications explicites de propriété n'ont pas besoin de validation supplémentaire.

Blueshift © 2025Commit: e573eab