Ejecución de CPI y privilegios

Resumen

Los CPI pasan por 11 pasos del runtime incluyendo verificación de privilegios, traducción de cuentas y sincronización de datos. Profundidad máxima de llamadas: 5 (9 con SIMD-0268). Las reglas de privilegios evitan que el destinatario escale más allá de lo que el llamador otorgó.

Reglas de privilegios

Los CPI extienden los privilegios de cuenta del llamador al destinatario con aplicación estricta. El runtime verifica estas reglas en prepare_next_instruction:

Escenario¿Permitido?Punto de aplicaciónError
El llamador pasa la cuenta como escribible, el destinatario la marca como escribible----
El llamador pasa la cuenta como solo lectura, el destinatario la marca como escribibleNoprepare_next_instructionPrivilegeEscalation
El llamador pasa la cuenta como escribible, el destinatario la marca como solo lectura----
El llamador pasa la cuenta como firmante, el destinatario la marca como firmante----
El llamador pasa la cuenta como no firmante, el destinatario la marca como firmante, la cuenta es un PDA derivado de las semillas del llamadorprepare_next_instruction--
El llamador pasa la cuenta como no firmante, el destinatario la marca como firmante, la cuenta NO es un PDA del llamadorNoprepare_next_instructionPrivilegeEscalation
El llamador pasa la cuenta como firmante, el destinatario la marca como no firmante----
El programa A se llama a sí mismo directamente (A -> A)push()--
El programa A llama a B que llama a A (reentrada indirecta)Nopush()ReentrancyNotAllowed
CPI a native loader, bpf_loader, bpf_loader_deprecated o precompileNocheck_authorized_programProgramNotSupported
Cuenta no encontrada en la transacciónNoprepare_next_instructionMissingAccount

Las reglas de privilegios se pueden resumir como:

  1. El privilegio de escritura no puede escalar. Si el llamador marca una cuenta como solo lectura, el llamado no puede marcarla como escribible.
  2. El privilegio de firmante requiere autorización. Una cuenta puede ser firmante en el llamado solo si (a) ya era firmante en el llamador, O (b) es un PDA derivado de las semillas del programa llamador mediante invoke_signed.
  3. La reducción de privilegios siempre está permitida. El llamado puede usar menos privilegios de los que el llamador otorgó.

Flujo de ejecución de CPI

Un CPI pasa a través de varias capas de runtime. Esta sección documenta el pipeline completo desde la llamada SDK del programa a través del límite de syscall hacia el runtime y de vuelta. Cada paso hace referencia al archivo fuente que lo implementa.

La altura máxima de la invocación de instrucción del programa se llama max_instruction_stack_depth y está establecida en la constante MAX_INSTRUCTION_STACK_DEPTH de 5. Con MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 activo, esto aumenta a 9.

La altura de pila 1 es la instrucción de transacción inicial. Cada CPI incrementa la altura en 1. Un máximo de 5 significa que un programa puede hacer CPIs hasta 4 niveles de profundidad (8 niveles de profundidad con SIMD-0268).

Paso 1: El programa llama a invoke o invoke_signed

El programa llama a invoke o invoke_signed. invoke es un wrapper delgado que llama a invoke_signed con un array de semillas de firmante vacío. La función SDK serializa la Instruction, el slice AccountInfo y las semillas de firmante en la memoria de la VM, luego activa la syscall.

Paso 2: Entrada de syscall

La VM SBF despacha al manejador de syscall sol_invoke_signed_rust, que llama al punto de entrada compartido: cpi_common.

Paso 3: consumir el costo de invocación

La primera acción dentro de cpi_common es cobrar el costo fijo de invocación del medidor de cómputo compartido: invoke_units = 1.000 CU (o 946 CU con SIMD-0339).

Paso 4: traducir la instrucción desde la memoria de la VM

El manejador de syscall traduce la instrucción desde el espacio de direcciones de la VM del programa a tipos Rust del lado del host mediante translate_instruction_rust, que lee una estructura StableInstruction, valida la longitud de los datos contra MAX_INSTRUCTION_DATA_LEN (10.240 bytes), y luego cobra el costo de serialización de datos.

Paso 5: traducir las seeds de firmante y derivar PDA

El manejador llama a translate_signers_rust. Para cada conjunto de seeds de firmante, el runtime:

  1. Verifica el número de conjuntos de seeds de firmante contra MAX_SIGNERS (16).
  2. Verifica la longitud de cada conjunto de seeds contra MAX_SEEDS (16 seeds por conjunto).
  3. Llama a Pubkey::create_program_address con las seeds y el ID del programa del llamador. Si las seeds no producen un PDA válido, el CPI falla con BadSeeds.
  4. Recopila las pubkeys de PDA resultantes en un vec signers.

Estos PDA derivados se tratan como firmantes válidos para la instrucción del destinatario.

Paso 6: verificar el programa autorizado

Antes de continuar, el runtime llama a check_authorized_program para verificar que el programa de destino esté permitido para CPI. Los siguientes programas están bloqueados:

  • El cargador nativo
  • bpf_loader e bpf_loader_deprecated
  • bpf_loader_upgradeable (excepto instrucciones de gestión específicas: upgrade, set_authority, set_authority_checked (con feature gate), extend_program_checked (con feature gate), close)
  • Programas de precompilación (ed25519, secp256k1, etc.)

La violación devuelve ProgramNotSupported.

Paso 7: verificación de privilegios (prepare_next_instruction)

El runtime llama a prepare_next_instruction que construye la lista InstructionAccount del destinatario y aplica las reglas de privilegios. Consulta Reglas de privilegios a continuación para la tabla de decisión completa.

Paso 8: traducir información de cuentas

El manejador llama a translate_accounts que:

  1. Valida el recuento de información de cuentas contra MAX_CPI_ACCOUNT_INFOS (128, o 255 con SIMD-0339).
  2. Cobra el costo de traducción de información de cuentas (solo SIMD-0339): (num_account_infos * 80) / 250 CUs.
  3. Para cada cuenta no ejecutable y no duplicada, construye un CallerAccount traduciendo punteros de la memoria de la VM a la memoria del host. Esto incluye cobrar el costo de serialización de datos por cuenta: account_data_len / cpi_bytes_per_unit CUs.

Paso 9: sincronización de cuentas pre-CPI (llamador a llamado)

Antes de ejecutar el llamado, el runtime sincroniza las modificaciones de cuenta del llamador para que el llamado pueda verlas. La función update_callee_account se llama para cada cuenta traducida, copiando lamports, datos y propietario. Consulta Sincronización de datos de cuentas para el mapeo detallado de campos.

Paso 10: agregar contexto de instrucción, ejecutar llamado y quitar

El runtime llama a process_instruction, que:

  1. Llama a push() para agregar un nuevo marco a la pila de instrucciones. push() aplica la regla de reentrada: un programa solo puede llamarse a sí mismo si es el llamador directo (es decir, el programa A puede llamar a A, pero A no puede llamar a B que llama a A). La violación devuelve ReentrancyNotAllowed.
  2. Llama a process_executable_chain que resuelve el punto de entrada del programa llamado y lo invoca. El llamado se ejecuta con el mismo medidor de cómputo compartido. Todo el consumo de CU por parte del llamado reduce el presupuesto restante del llamador.
  3. Llama a pop() para quitar el marco del llamado y verificar que los saldos de lamports no hayan cambiado (UnbalancedInstruction si no).

Paso 11: sincronización de cuentas post-CPI (llamado a llamador)

Después de que process_instruction retorna (lo que incluye el pop), el runtime sincroniza los cambios de vuelta al llamador mediante update_caller_account para cada cuenta escribible. Además, update_caller_account_region actualiza los mapeos de regiones de memoria de la VM para cuentas cuyos regiones de datos cambiaron. Consulta Sincronización de datos de cuentas para el mapeo detallado de campos.

La syscall CPI devuelve 0 (éxito) al programa llamador.

Is this page helpful?

Gestionado por

© 2026 Fundación Solana.
Todos los derechos reservados.
Conéctate