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ón | Error |
|---|---|---|---|
| El llamador pasa la cuenta como escribible, el destinatario la marca como escribible | Sí | -- | -- |
| El llamador pasa la cuenta como solo lectura, el destinatario la marca como escribible | No | prepare_next_instruction | PrivilegeEscalation |
| El llamador pasa la cuenta como escribible, el destinatario la marca como solo lectura | Sí | -- | -- |
| El llamador pasa la cuenta como firmante, el destinatario la marca como firmante | Sí | -- | -- |
| El llamador pasa la cuenta como no firmante, el destinatario la marca como firmante, la cuenta es un PDA derivado de las semillas del llamador | Sí | prepare_next_instruction | -- |
| El llamador pasa la cuenta como no firmante, el destinatario la marca como firmante, la cuenta NO es un PDA del llamador | No | prepare_next_instruction | PrivilegeEscalation |
| El llamador pasa la cuenta como firmante, el destinatario la marca como no firmante | Sí | -- | -- |
| El programa A se llama a sí mismo directamente (A -> A) | Sí | push() | -- |
| El programa A llama a B que llama a A (reentrada indirecta) | No | push() | ReentrancyNotAllowed |
| CPI a native loader, bpf_loader, bpf_loader_deprecated o precompile | No | check_authorized_program | ProgramNotSupported |
| Cuenta no encontrada en la transacción | No | prepare_next_instruction | MissingAccount |
Las reglas de privilegios se pueden resumir como:
- El privilegio de escritura no puede escalar. Si el llamador marca una cuenta como solo lectura, el llamado no puede marcarla como escribible.
- 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. - 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:
- Verifica el número de conjuntos de seeds de firmante contra
MAX_SIGNERS(16). - Verifica la longitud de cada conjunto de seeds contra
MAX_SEEDS(16 seeds por conjunto). - Llama a
Pubkey::create_program_addresscon las seeds y el ID del programa del llamador. Si las seeds no producen un PDA válido, el CPI falla conBadSeeds. - 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_loaderebpf_loader_deprecatedbpf_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:
- Valida el recuento de información de cuentas
contra
MAX_CPI_ACCOUNT_INFOS(128, o 255 con SIMD-0339). - Cobra el costo de traducción de información de cuentas
(solo SIMD-0339):
(num_account_infos * 80) / 250CUs. - Para cada cuenta no ejecutable y no duplicada, construye un
CallerAccounttraduciendo 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_unitCUs.
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:
- 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 devuelveReentrancyNotAllowed. - Llama a
process_executable_chainque 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. - Llama a
pop()para quitar el marco del llamado y verificar que los saldos de lamports no hayan cambiado (UnbalancedInstructionsi 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?