Resumo
CPIs passam por 11 etapas de runtime incluindo verificação de privilégios, tradução de contas e sincronização de dados. Profundidade máxima de chamada: 5 (9 com SIMD-0268). Regras de privilégio impedem que o programa chamado escale além do que o chamador concedeu.
Regras de privilégio
CPIs estendem os privilégios de conta do chamador para o programa chamado com
aplicação rigorosa. O runtime verifica essas regras em
prepare_next_instruction:
| Cenário | Permitido? | Ponto de aplicação | Erro |
|---|---|---|---|
| Chamador passa conta como gravável, programa chamado marca como gravável | Sim | -- | -- |
| Chamador passa conta como somente leitura, programa chamado marca como gravável | Não | prepare_next_instruction | PrivilegeEscalation |
| Chamador passa conta como gravável, programa chamado marca como somente leitura | Sim | -- | -- |
| Chamador passa conta como signatário, programa chamado marca como signatário | Sim | -- | -- |
| Chamador passa conta como não signatário, programa chamado marca como signatário, conta é um PDA derivado das seeds do chamador | Sim | prepare_next_instruction | -- |
| Chamador passa conta como não signatário, programa chamado marca como signatário, conta NÃO é um PDA do chamador | Não | prepare_next_instruction | PrivilegeEscalation |
| Chamador passa conta como signatário, programa chamado marca como não signatário | Sim | -- | -- |
| Programa A chama a si mesmo diretamente (A -> A) | Sim | push() | -- |
| Programa A chama B que chama A (reentrância indireta) | Não | push() | ReentrancyNotAllowed |
| CPI para native loader, bpf_loader, bpf_loader_deprecated ou precompile | Não | check_authorized_program | ProgramNotSupported |
| Conta não encontrada na transação | Não | prepare_next_instruction | MissingAccount |
As regras de privilégio podem ser resumidas como:
- Privilégio de escrita não pode escalar. Se o chamador marcar uma conta como somente leitura, o chamado não pode marcá-la como gravável.
- Privilégio de assinante requer autorização. Uma conta pode ser assinante
no chamado apenas se (a) já era assinante no chamador, OU (b) é um PDA
derivado das seeds do programa chamador via
invoke_signed. - Redução de privilégio é sempre permitida. O chamado pode usar menos privilégios do que o chamador concedeu.
Fluxo de execução CPI
Um CPI passa por várias camadas de runtime. Esta seção documenta o pipeline completo desde a chamada SDK do programa através do limite de syscall até o runtime e de volta. Cada etapa referencia o arquivo fonte que a implementa.
A altura máxima da invocação de instrução do programa é chamada de
max_instruction_stack_depth
e é definida como a
constante MAX_INSTRUCTION_STACK_DEPTH
de 5. Com MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 ativo, isso aumenta para 9.
Altura de pilha 1 é a instrução de transação inicial. Cada CPI incrementa a altura em 1. Um máximo de 5 significa que um programa pode fazer CPIs até 4 níveis de profundidade (8 níveis de profundidade com SIMD-0268).
Etapa 1: Programa chama invoke ou invoke_signed
O programa chama
invoke
ou
invoke_signed.
invoke é um wrapper fino que chama invoke_signed com um array vazio
de seeds de assinante. A função SDK serializa a Instruction, slice
AccountInfo e seeds de assinante na memória da VM, depois aciona a
syscall.
Etapa 2: Entrada de syscall
A VM SBF despacha para o handler de syscall
sol_invoke_signed_rust,
que chama o ponto de entrada compartilhado:
cpi_common.
Passo 3: Consumir custo de invocação
A primeira ação dentro de cpi_common é
cobrar o custo fixo de invocação
do medidor de computação compartilhado:
invoke_units
= 1.000 CUs (ou 946 CUs com SIMD-0339).
Passo 4: Traduzir instrução da memória da VM
O manipulador de syscall traduz a instrução do espaço de endereçamento da VM do
programa para tipos Rust do lado do host através de
translate_instruction_rust,
que lê uma estrutura StableInstruction, valida o comprimento dos dados
contra
MAX_INSTRUCTION_DATA_LEN
(10.240 bytes), e então cobra o
custo de serialização de dados.
Passo 5: Traduzir seeds de assinante e derivar PDAs
O manipulador chama
translate_signers_rust.
Para cada conjunto de seeds de assinante, o runtime:
- Verifica o número de conjuntos de seeds de assinante contra
MAX_SIGNERS(16). - Verifica o comprimento de cada conjunto de seeds contra
MAX_SEEDS(16 seeds por conjunto). - Chama
Pubkey::create_program_addresscom as seeds e o ID do programa do chamador. Se as seeds não produzirem um PDA válido, o CPI falha comBadSeeds. - Coleta as pubkeys PDA resultantes num vec
signers.
Estes PDAs derivados são tratados como assinantes válidos para a instrução do chamado.
Passo 6: Verificar programa autorizado
Antes de prosseguir, o runtime chama
check_authorized_program
para verificar se o programa alvo é permitido para CPI. Os seguintes programas
são bloqueados:
- O carregador nativo
bpf_loaderebpf_loader_deprecatedbpf_loader_upgradeable(exceto instruções específicas de gestão:upgrade,set_authority,set_authority_checked(com feature gate),extend_program_checked(com feature gate),close)- Programas de pré-compilação (ed25519, secp256k1, etc.)
A violação retorna
ProgramNotSupported.
Passo 7: Verificação de privilégios (prepare_next_instruction)
O runtime chama
prepare_next_instruction
que constrói a lista InstructionAccount do chamado e aplica as regras de
privilégios. Consulte Regras de privilégios abaixo para a
tabela de decisão completa.
Passo 8: Traduzir informações da conta
O manipulador chama translate_accounts que:
- Valida a contagem de informações da conta
contra
MAX_CPI_ACCOUNT_INFOS(128, ou 255 com SIMD-0339). - Cobra o custo de tradução de informações da conta
(apenas SIMD-0339):
(num_account_infos * 80) / 250CUs. - Para cada conta não executável e não duplicada, constrói um
CallerAccounttraduzindo ponteiros da memória da VM para a memória do host. Isso inclui cobrar o custo de serialização de dados por conta:account_data_len / cpi_bytes_per_unitCUs.
Passo 9: Sincronização de conta pré-CPI (chamador para chamado)
Antes de executar o chamado, o runtime sincroniza as modificações da conta do
chamador para que o chamado possa vê-las. A função
update_callee_account
é chamada para cada conta traduzida, copiando lamports, dados e proprietário.
Consulte
Sincronização de dados da conta
para o mapeamento detalhado dos campos.
Passo 10: Empilhar contexto de instrução, executar chamado e desempilhar
O runtime chama
process_instruction,
que:
- Chama
push()para adicionar um novo frame à pilha de instruções.push()impõe a regra de reentrada: um programa só pode chamar a si mesmo se for o chamador direto (ou seja, o programa A pode chamar A, mas A não pode chamar B que chama A). A violação retornaReentrancyNotAllowed. - Chama
process_executable_chainque resolve o ponto de entrada do programa chamado e o invoca. O chamado executa com o mesmo medidor de computação compartilhado. Todo o consumo de CU pelo chamado reduz o orçamento restante do chamador. - Chama
pop()para remover o frame do chamado e verificar se os saldos de lamport permanecem inalterados (UnbalancedInstructioncaso contrário).
Passo 11: Sincronização de conta pós-CPI (chamado para chamador)
Após process_instruction retornar (o que inclui o desempilhamento), o runtime
sincroniza as alterações de volta para o chamador via
update_caller_account
para cada conta gravável. Além disso,
update_caller_account_region
atualiza os mapeamentos de região de memória da VM para contas cujas regiões de
dados foram alteradas. Consulte
Sincronização de dados da conta
para o mapeamento detalhado dos campos.
O syscall CPI retorna 0 (sucesso) ao programa chamador.
Is this page helpful?