Anclaje de CPIs en Detalle
Una invocación entre programas (CPI) es el primitivo de composición de Solana: durante tu instrucción, tu programa llama a otro programa con una lista de cuentas, datos de instrucción y semillas de firmante PDA opcionales. Anchor 0.32.1 envuelve la ruta invoke / invoke_signed nativa en CpiContext, ayudantes generados y cuentas de programa tipadas para que la composición sea legible sin ocultar las reglas de tiempo de ejecución.
Esta página es el mapa de la sección. Úsala para colocar CpiContext, CPIs firmadas por PDA, ayudantes de Token SPL, módulos de programa personalizados, cuentas restantes y verificaciones de privilegios en un continuo antes de seguir las páginas de recetas enfocadas.
Resumen
Las CPIs de Anchor todavía obedecen las mismas reglas de SVM que el código nativo. El llamado debe ser una cuenta de programa ejecutable. Cada cuenta que el llamado necesita debe aparecer en la instrucción del llamador (o estar anidada correctamente a través de CPIs adicionales). Los privilegios de firmante provienen de las firmas de la transacción o de invoke_signed con semillas que prueban que tu programa posee una PDA. Las cuentas mutables deben marcarse como escribibles en la transacción externa cuando el llamado las escriba.
Lo que Anchor añade es el empaquetado. CpiContext::new contiene la cuenta AccountInfo del programa y una estructura de cuentas. CpiContext::new_with_signer (o .with_signer) adjunta porciones de semillas para que el tiempo de ejecución trate a la PDA como firmante para esa instrucción interna. anchor-spl construye diseños estándar de Token / Token-2022. declare_program! convierte una IDL externa en un módulo cpi con estructuras de cuentas tipadas. .with_remaining_accounts anexa cuentas dinámicas para enrutadores, oráculos y flujos de varios saltos.
La superficie de riesgo está concentrada. Un ID de programa incorrecto es código arbitrario en tu contexto de privilegios. Semillas de PDA incorrectas fallan la CPI o autorizan la cuenta incorrecta. Las cuentas restantes no validadas son ataques de sustitución. Firmar como una PDA de tesorería en un programa no confiable es un vaciado de bóveda. Trata cada CPI como un límite de privilegios: ancla los objetivos, valida las identidades y mapea los errores intencionalmente.
Fundamentos
Qué es una CPI (y qué no es)
Una CPI es ejecución anidada síncrona dentro de una instrucción de transacción. Tu manejador se ejecuta, luego el tiempo de ejecución entra en el llamado, luego el control regresa para que puedas continuar o fallar. No es una segunda instrucción de transacción de nivel superior. La atomicidad todavía se aplica: si la instrucción externa falla después de una CPI interna exitosa, toda la instrucción se aborta y el estado se revierte para esa ruta de fallo.
Los clientes a menudo componen múltiples instrucciones de nivel superior en su lugar. Eso no es CPI. La CPI es necesaria cuando la lógica en cadena debe decidir la llamada, aplicar verificaciones intermedias o firmar como una PDA que el usuario no puede firmar.
Por qué Anchor envuelve invoke
El código nativo construye una Instruction { program_id, accounts, data } y llama a invoke o invoke_signed. Anchor genera estas piezas a partir de:
| Pieza | Representación de Anchor |
|---|---|
| Programa de destino | Primer argumento de CpiContext::new / cuenta Program<T> |
| Metas de cuenta | Campos de estructura convertidos a través de ToAccountInfos / ToAccountMetas |
| Datos de instrucción | Argumentos de función en token::transfer, cpi::foo generado, etc. |
| Firmantes PDA | signer_seeds en el contexto |
Todavía pagas unidades de cómputo por el programa interno y aún debes liberar préstamos conflictivos antes de la CPI en cuentas que también mutas localmente.
Modelo de privilegios en una tabla
| Fuente de privilegio | Cómo aparece en la CPI |
|---|---|
| Firma de usuario / clave | Cuenta is_signer verdadera de la transacción externa |
| PDA de tu programa | invoke_signed / CpiContext::new_with_signer con semillas coincidentes |
| PDA de otro programa | No puedes firmarla; llama a CPI al programa propietario en su lugar |
| Escribible | AccountMeta externa debe permitir escrituras si el llamado escribe |
El tiempo de ejecución no inventa autoridad. Solo verifica las firmas ya presentes y las pruebas de semillas que proporcionas para tu ID de programa.
Dónde se ubica cada página de receta
| Tema | Rol |
|---|---|
| Fundamentos de CPI en Anchor | Patrones de CpiContext y primeros ejemplos |
| CPIs de Token SPL | anchor-spl transferencia, emisión, quema, interfaz Token-2022 |
| CPIs Firmadas | Semillas PDA, bumps, grupos de múltiples firmantes |
| CPI a Programas Personalizados | declare_program! y módulos anclados por IDL |
| Pasar Cuentas Restantes | Colas de cuentas dinámicas |
| Seguridad de CPI | Anclaje de programas, anti-reentrada, límites de privilegios |
Mecánicas
Una CPI es un pipeline fijo: validar cuentas externas, construir contexto, semillas y cuentas restantes opcionales, invocar, manejar el resultado.
Pipeline de CPI de principio a fin
Manejador de instrucción externa (tu programa)
|
| 1. las restricciones ya se ejecutaron en las cuentas del Contexto
| 2. verificaciones opcionales de remaining_accounts
| 3. construir CpiContext (programa + cuentas)
| 4. opcional with_signer / new_with_signer
| 5. opcional with_remaining_accounts
v
Tiempo de ejecución: invoke / invoke_signed
|
v
Callee process_instruction
|
| Ok -> continuar manejador externo
| Err -> el externo ve ProgramError (mapear o propagar)
v
Outer Ok(()) confirma la instrucción | cualquier Err aborta la instrucción
Valida antes de la CPI sensible a privilegios siempre que sea posible. Prefiere el estilo de verificaciones-efectos-interacciones cuando un llamado pueda volver a entrar en tu programa.
1. CpiContext::new
Las CPIs sin firmar (o firmadas por el usuario) usan CpiContext::new(program, accounts):
use anchor_lang::system_program::{self, Transfer};
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
},
),
lamports,
)?;El primer argumento es la cuenta de programa ejecutable, no una clave pública aleatoria. Prefiere Program<'info, System> (o Token, o tipos de programa generados) para que Anchor rechace IDs de programa incorrectos en el momento de la restricción. Ver Fundamentos de CPI en Anchor.
2. CPIs Firmadas con Semillas PDA
Cuando una PDA es la autoridad (bóveda, autoridad de emisión, escrow), pasa semillas que el tiempo de ejecución puede hashear de vuelta a esa dirección bajo tu ID de programa:
let bump = ctx.bumps.vault;
let seeds: &[&[u8]] = &[b"vault", user.key().as_ref(), &[bump]];
let signer = &[seeds];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault_ata.to_account_info(),
to: ctx.accounts.user_ata.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer,
),
amount,
)?;Reglas que importan en producción:
- El orden y los componentes de las semillas deben coincidir con la derivación de la PDA y las restricciones
#[account(seeds = ...)]. - Prefiere
ctx.bumps.*sobre los bumps codificados en duro después de la inicialización. - La cuenta PDA debe ser el campo de autoridad que el llamado espera.
- Múltiples PDAs necesitan múltiples grupos de semillas en el slice de firmante.
Estilo equivalente: construir CpiContext::new(...) y luego .with_signer(signer). Elige un estilo por base de código. Patrones completos: CPIs Firmadas.
3. CPIs de Token SPL a través de anchor-spl
anchor-spl 0.32.1 expone ayudantes como transfer, mint_to, burn y approve que empaquetan las metas de cuenta y los datos de instrucción correctos para el programa Token SPL.
use anchor_spl::token::{self, Transfer};
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.from_ata.to_account_info(),
to: ctx.accounts.to_ata.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
),
amount,
)?;Lista de verificación operativa:
- Marca las cuentas de token de origen y destino como
mut. - La autoridad debe ser un firmante o una PDA con
with_signer. - Valida la alineación de los mint entre las ATAs cuando las cantidades representan el mismo activo.
- Para Token-2022 y soporte dual, usa
anchor_spl::token_interfaceconInterface<'info, TokenInterface>en lugar de codificar solo el Token clásico.
Profundidad de la receta: CPIs de Token SPL.
4. CPI a programas personalizados
Para otros programas de Anchor (o compatibles con IDL), declare_program!(name) de Anchor 0.32.1 lee la IDL JSON en tiempo de compilación y genera funciones name::cpi::... y estructuras name::cpi::accounts::....
declare_program!(marketplace);
marketplace::cpi::list_item(
CpiContext::new(
ctx.accounts.marketplace_program.to_account_info(),
marketplace::cpi::accounts::ListItem { /* reflejar cuentas IDL */ },
),
price,
)?;Ancla la IDL a la versión desplegada del programa. Las IDLs obsoletas compilan limpiamente y fallan (o peor, decodifican incorrectamente) en tiempo de ejecución. Usa el tipo generado Program<'info, marketplace::program::Marketplace> (los nombres varían según la IDL) para que el ID del programa no pueda ser sustituido. Los llamados que no son de Anchor aún pueden necesitar discriminadores manuales e invoke / invoke_signed. Ver CPI a Programas Personalizados.
5. Cuentas restantes
Algunos llamados esperan una cola variable: rutas de salto, conjuntos de oráculos, tarifas opcionales. Anchor expone las cuentas más allá de la estructura fija #[derive(Accounts)] como ctx.remaining_accounts. Reenvíalas con:
let mut cpi_ctx = CpiContext::new(
ctx.accounts.target_program.to_account_info(),
target::cpi::accounts::Handle { /* campos fijos */ },
);
cpi_ctx = cpi_ctx.with_remaining_accounts(ctx.remaining_accounts.to_vec());
target::cpi::handle(cpi_ctx)?;Antes de reenviar:
- Limita la longitud (
require!(len <= MAX)). - Verifica listas blancas de propietarios, claves, discriminadores y frescura según lo requiera el modelo de amenazas.
- Conserva el orden documentado por el llamado y por tus constructores de clientes (
@solana/kit0.7.0 o clientes de Anchor).
Detalle: Pasar Cuentas Restantes.
6. Errores y cómputo
Los fallos internos aparecen como ProgramError (o códigos de error de Anchor) para el llamador. Mapea fallos opacos a tus variantes #[error_code] cuando la experiencia de usuario del producto necesite códigos de cliente claros. La simulación muestra registros anidados; no confíes en msg! verbosos en rutas de producción.
Cada CPI consume CU para la configuración y para el trabajo del llamado. Las CPIs anidadas y los recorridos extensos de cuentas restantes suman. Presupuesta y mide las instrucciones "calientes" bajo las herramientas locales de Agave 4.1.1 antes de la red principal.
Avanzado
Reentrada y estado compartido
Los llamados pueden hacer CPI de vuelta a tu programa si los clientes (o programas intermedios) incluyen tu ID de programa y cuentas. Diseña para la reentrada: establece indicadores de "en progreso" antes de la CPI, completa las transiciones de estado críticas antes de ceder, y nunca asumas invariantes que una entrada recursiva pueda romper a mitad de camino. Esto es especialmente agudo para bóvedas, emisiones y cuentas de configuración compartidas.
Minimización de privilegios
Firmar como una PDA de alto valor es una concesión deliberada de autoridad. Define el alcance de qué instrucciones pueden usar new_with_signer para semillas de tesorería. Nunca adjuntes semillas de tesorería a una CPI cuyo ID de programa o selector de instrucción sea controlado por el cliente. Prefiere autoridades estrechas (una PDA de autoridad de emisión, una PDA de bóveda) sobre una única PDA "dios" utilizada para cada llamada externa.
Composición híbrida
Forma común en producción:
- Depósito de token firmado por el usuario en una ATA de bóveda (sin firma PDA).
- Lógica del programa actualiza el estado interno.
- CPI firmada por PDA a un módulo IDL de DEX, mercado de préstamos o mercado.
- Cuentas restantes opcionales para saltos de ruta validadas contra una lista blanca.
Documenta para los auditores qué IDs de programa son alcanzables, qué semillas firman y qué formas de cuentas restantes pueden agregar los clientes.
Pruebas de CPIs
Las pruebas deben cubrir más que los caminos felices: ID de programa incorrecto, mint incorrecto, bump incorrecto, falta de mut, intercambios de orden de cuentas restantes y errores personalizados del llamado. Los validadores locales bajo Solana CLI 3.0.10 ejercitan SBF completo y registros. Mantén las "fixtures" de IDL en el repositorio para declare_program! para que la CI no se desvíe de los diseños desplegados sin un bump deliberado.
Clientes y metadatos de cuenta
Las transacciones externas construidas con @solana/kit 0.7.0 (o Anchor TS) deben listar cada cuenta que el árbol completo de CPI tocará, con las banderas correctas de firmante y escribible. Una bandera escribible faltante falla en tiempo de ejecución incluso si los tipos de Rust parecen correctos. Cuando las cuentas restantes importan, publica el contrato de ordenación junto a la IDL.
Conceptos erróneos comunes
Concepto erróneo: Las restricciones de Anchor validan completamente el objetivo de la CPI. Las restricciones validan las cuentas que declaraste. No validan automáticamente todas las cuentas restantes ni todas las reglas de negocio dentro de un programa de terceros.
Concepto erróneo: Cualquier PDA puede firmar si paso semillas. Solo las PDAs derivadas bajo tu ID de programa pueden ser firmadas por tu programa. La PDA de otro programa requiere una CPI a ese propietario.
Concepto erróneo: Program<'info, Token> es opcional si codifico en duro la clave del programa Token en los datos.
Las claves codificadas en duro en los datos de instrucción no son lo mismo que anclar la cuenta ejecutable. Siempre pasa y verifica el tipo de la cuenta del programa.
Concepto erróneo: La CPI firmada es solo para transferencias de SOL. Cualquier instrucción que necesite una autoridad PDA (transferencia de token, emisión, quema, aprobación, lista de mercado personalizada) utiliza el mismo mecanismo de semillas.
Concepto erróneo: Las cuentas restantes son "no verificadas, por lo que son gratuitas". No son verificadas por las macros de cuenta de Anchor hasta que tú las verificas. Las colas no validadas son una clase de exploit de primer nivel.
Concepto erróneo: La composición de múltiples instrucciones del cliente reemplaza la CPI para la custodia de PDAs. Si solo el programa puede firmar como la bóveda, el cliente no puede enviar una transferencia de token independiente como esa bóveda. Se requiere CPI con semillas.
Concepto erróneo: Mapear cada error de CPI a un único SomethingFailed está bien.
Los prototipos pueden. Los clientes de producción necesitan códigos específicos y estables para reintentos y respuesta a incidentes.
Concepto erróneo: declare_program! congela la seguridad para siempre.
Las actualizaciones del llamado cambian el comportamiento bajo el mismo ID de programa. Reaudita después de las actualizaciones, incluso cuando la IDL todavía se analiza.
Preguntas frecuentes
¿Qué es una CPI en términos de Anchor?
Una llamada tipada a otro programa construida con CpiContext (y generalmente un módulo auxiliar) que se compila a invoke o invoke_signed en tiempo de ejecución.
¿Cuándo uso CpiContext::new en lugar de new_with_signer?
Usa new cuando todos los firmantes requeridos ya firmaron la transacción externa. Usa new_with_signer (o .with_signer) cuando una PDA de tu programa deba autorizar la instrucción interna.
¿De dónde provienen las semillas de firmante?
Los mismos componentes de semillas utilizados para derivar la PDA, más el bump (a menudo de ctx.bumps). Deben coincidir exactamente con la derivación en cadena.
¿Cómo funcionan las transferencias de Token SPL desde un programa?
Incluye las cuentas de token y Program<Token> (o interfaz Token), construye una estructura de cuenta Transfer y llama a token::transfer con un CpiContext. Las autoridades PDA necesitan semillas de firmante.
¿Para qué sirve declare_program!?
Genera clientes CPI en tiempo de compilación a partir de la IDL de un programa externo para que obtengas estructuras de cuentas tipadas y ayudantes de instrucción en lugar de discriminadores construidos manualmente.
¿Cómo se relacionan las cuentas restantes con la IDL?
Las cuentas fijas viven en la estructura de cuentas y la IDL. Las cuentas adicionales que los clientes agregan después de esa lista se convierten en remaining_accounts y deben ser validadas y ordenadas según las reglas del llamado.
¿Puede una CPI fallar mientras mis escrituras de estado anteriores permanecen?
Si la instrucción externa devuelve Err después de una CPI, la instrucción falla como una unidad. Estructura el código para que no dependas del éxito parcial cuando la ruta externa aún pueda fallar.
¿Por qué anclar Program<T> en lugar de UncheckedAccount para los programas?
Program<T> verifica que la cuenta sea el ID ejecutable esperado (y ejecutable). Las cuentas de programa no verificadas invitan a ataques de sustitución.
¿Qué tan profundo pueden anidarse las CPIs?
El tiempo de ejecución impone una profundidad máxima de llamada CPI. Diseña para árboles poco profundos; los gráficos dinámicos profundos son difíciles de razonar para CU y reentrada.
¿Necesito CPI para leer la cuenta de otro programa?
No. Leer datos de cuenta que se te pasaron no requiere CPI. La CPI es para ejecutar la lógica de instrucción de otro programa.
¿Cómo deben los clientes preparar las cuentas para instrucciones con muchas CPIs?
Lista cada cuenta que el programa externo y todos los llamados anidados necesiten, con las banderas correctas de firmante/escribible, coincidiendo con la IDL del programa y la especificación de cuentas restantes, utilizando @solana/kit 0.7.0 o herramientas de cliente Anchor.
¿A dónde debo ir ahora en esta sección?
Comienza con Fundamentos de CPI en Anchor, luego CPIs de Token SPL y CPIs Firmadas. Agrega CPI a Programas Personalizados y Pasar Cuentas Restantes al componer externamente. Cierra el ciclo con Seguridad de CPI.
Relacionado
- Fundamentos de CPI en Anchor - Constructores de
CpiContexty primeros patrones de trabajo - CPIs de Token SPL -
anchor-spltransferencia, emisión, quema e interfaz Token-2022 - CPIs Firmadas - Semillas PDA, bumps y disciplina de
with_signer - CPI a Programas Personalizados - Módulos IDL de
declare_program!e higiene de actualizaciones - Pasar Cuentas Restantes - Colas dinámicas, orden y límites
- Seguridad de CPI - Anclaje de programas, reentrada y límites de privilegios
Versiones de Stack: Esta página fue escrita para Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, y @solana/kit 0.7.0.