Résumé
Les CPI passent par 11 étapes du runtime incluant la vérification des privilèges, la traduction des comptes et la synchronisation des données. Profondeur d'appel maximale : 5 (9 avec SIMD-0268). Les règles de privilèges empêchent l'appelé d'escalader au-delà de ce que l'appelant a accordé.
Règles de privilèges
Les CPI étendent les privilèges de compte de l'appelant à l'appelé avec une
application stricte. Le runtime vérifie ces règles dans
prepare_next_instruction
:
| Scénario | Autorisé ? | Point d'application | Erreur |
|---|---|---|---|
| L'appelant passe le compte en écriture, l'appelé le marque en écriture | Oui | -- | -- |
| L'appelant passe le compte en lecture seule, l'appelé le marque en écriture | Non | prepare_next_instruction | PrivilegeEscalation |
| L'appelant passe le compte en écriture, l'appelé le marque en lecture seule | Oui | -- | -- |
| L'appelant passe le compte comme signataire, l'appelé le marque comme signataire | Oui | -- | -- |
| L'appelant passe le compte comme non-signataire, l'appelé le marque comme signataire, le compte est un PDA dérivé des seeds de l'appelant | Oui | prepare_next_instruction | -- |
| L'appelant passe le compte comme non-signataire, l'appelé le marque comme signataire, le compte n'est PAS un PDA de l'appelant | Non | prepare_next_instruction | PrivilegeEscalation |
| L'appelant passe le compte comme signataire, l'appelé le marque comme non-signataire | Oui | -- | -- |
| Le programme A s'appelle lui-même directement (A -> A) | Oui | push() | -- |
| Le programme A appelle B qui appelle A (réentrance indirecte) | Non | push() | ReentrancyNotAllowed |
| CPI vers native loader, bpf_loader, bpf_loader_deprecated ou precompile | Non | check_authorized_program | ProgramNotSupported |
| Compte introuvable dans la transaction | Non | prepare_next_instruction | MissingAccount |
Les règles de privilège peuvent être résumées comme suit :
- Le privilège d'écriture ne peut pas être élevé. Si l'appelant marque un compte comme lecture seule, l'appelé ne peut pas le marquer comme accessible en écriture.
- Le privilège de signataire nécessite une autorisation. Un compte peut
être un signataire dans l'appelé uniquement si (a) il était déjà un
signataire dans l'appelant, OU (b) il s'agit d'un PDA dérivé des seeds du
programme appelant via
invoke_signed. - La réduction de privilège est toujours autorisée. L'appelé peut utiliser moins de privilèges que ceux accordés par l'appelant.
Flux d'exécution CPI
Un CPI traverse plusieurs couches d'exécution. Cette section documente le pipeline complet depuis l'appel SDK du programme à travers la limite syscall jusqu'au runtime et retour. Chaque étape fait référence au fichier source qui l'implémente.
La hauteur maximale de l'invocation d'instruction de programme est appelée
max_instruction_stack_depth
et est définie à la
constante MAX_INSTRUCTION_STACK_DEPTH
de 5. Avec MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 actif, cela augmente à 9.
La hauteur de pile 1 correspond à l'instruction de transaction initiale. Chaque CPI incrémente la hauteur de 1. Un maximum de 5 signifie qu'un programme peut effectuer des CPI jusqu'à 4 niveaux de profondeur (8 niveaux de profondeur avec SIMD-0268).
Étape 1 : le programme appelle invoke ou invoke_signed
Le programme appelle
invoke
ou
invoke_signed.
invoke est un wrapper léger qui appelle invoke_signed avec un
tableau de seeds de signataire vide. La fonction SDK sérialise
l'Instruction, le slice AccountInfo et les seeds de signataire dans
la mémoire VM, puis déclenche le syscall.
Étape 2 : entrée syscall
La VM SBF dispatche vers le gestionnaire syscall
sol_invoke_signed_rust,
qui appelle le point d'entrée partagé :
cpi_common.
Étape 3 : consommer le coût d'invocation
La première action à l'intérieur de cpi_common consiste à
facturer le coût d'invocation fixe
à partir du compteur de calcul partagé :
invoke_units
= 1 000 CU (ou 946 CU avec SIMD-0339).
Étape 4 : traduire l'instruction depuis la mémoire de la VM
Le gestionnaire de syscall traduit l'instruction depuis l'espace d'adressage de
la VM du programme vers les types Rust côté hôte via
translate_instruction_rust,
qui lit une structure StableInstruction, valide la longueur des données
par rapport à
MAX_INSTRUCTION_DATA_LEN
(10 240 octets), puis facture le
coût de sérialisation des données.
Étape 5 : traduire les seeds de signataire et dériver les PDA
Le gestionnaire appelle
translate_signers_rust.
Pour chaque ensemble de seeds de signataire, le runtime :
- Vérifie le nombre d'ensembles de seeds de signataire par rapport à
MAX_SIGNERS(16). - Vérifie la longueur de chaque ensemble de seeds par rapport à
MAX_SEEDS(16 seeds par ensemble). - Appelle
Pubkey::create_program_addressavec les seeds et l'ID de programme de l'appelant. Si les seeds ne produisent pas de PDA valide, le CPI échoue avecBadSeeds. - Collecte les clés publiques PDA résultantes dans un vec
signers.
Ces PDA dérivés sont traités comme des signataires valides pour l'instruction appelée.
Étape 6 : vérifier le programme autorisé
Avant de continuer, le runtime appelle
check_authorized_program
pour vérifier que le programme cible est autorisé pour le CPI. Les programmes
suivants sont bloqués :
- Le chargeur natif
bpf_loaderetbpf_loader_deprecatedbpf_loader_upgradeable(sauf instructions de gestion spécifiques :upgrade,set_authority,set_authority_checked(avec feature gate),extend_program_checked(avec feature gate),close)- Programmes de précompilation (ed25519, secp256k1, etc.)
Une violation renvoie
ProgramNotSupported.
Étape 7 : vérification des privilèges (prepare_next_instruction)
Le runtime appelle
prepare_next_instruction
qui construit la liste InstructionAccount de l'appelé et applique les
règles de privilèges. Voir Règles de privilèges ci-dessous
pour la table de décision complète.
Étape 8 : traduire les informations de compte
Le gestionnaire appelle translate_accounts qui :
- Valide le nombre d'informations de compte
par rapport à
MAX_CPI_ACCOUNT_INFOS(128, ou 255 avec SIMD-0339). - Facture le coût de traduction des informations de compte
(SIMD-0339 uniquement) :
(num_account_infos * 80) / 250CU. - Pour chaque compte non exécutable et non dupliqué, construit un
CallerAccounten traduisant les pointeurs de la mémoire VM vers la mémoire hôte. Cela inclut la facturation du coût de sérialisation des données par compte :account_data_len / cpi_bytes_per_unitCU.
Étape 9 : synchronisation pré-CPI des comptes (appelant vers appelé)
Avant d'exécuter l'appelé, le runtime synchronise les modifications de compte de
l'appelant afin que l'appelé puisse les voir. La fonction
update_callee_account
est appelée pour chaque compte traduit, copiant les lamports, les données et le
propriétaire. Voir
Synchronisation des données de compte
pour le mappage détaillé des champs.
Étape 10 : empiler le contexte d'instruction, exécuter l'appelé et dépiler
Le runtime appelle
process_instruction,
qui :
- Appelle
push()pour ajouter une nouvelle frame à la pile d'instructions.push()applique la règle de réentrance : un programme ne peut s'appeler lui-même que s'il est l'appelant direct (c'est-à-dire que le programme A peut appeler A, mais A ne peut pas appeler B qui appelle A). Une violation retourneReentrancyNotAllowed. - Appelle
process_executable_chainqui résout le point d'entrée du programme appelé et l'invoque. L'appelé s'exécute avec le même compteur de calcul partagé. Toute consommation de CU par l'appelé réduit le budget restant de l'appelant. - Appelle
pop()pour retirer la frame de l'appelé et vérifier que les soldes de lamports sont inchangés (UnbalancedInstructionsinon).
Étape 11 : synchronisation post-CPI des comptes (appelé vers appelant)
Après le retour de process_instruction (qui inclut le dépilage), le runtime
synchronise les modifications vers l'appelant via
update_caller_account
pour chaque compte modifiable. De plus,
update_caller_account_region
met à jour les mappages de régions mémoire VM pour les comptes dont les régions
de données ont changé. Voir
Synchronisation des données de compte
pour le mappage détaillé des champs.
Le syscall CPI renvoie 0 (succès) au programme appelant.
Is this page helpful?