Execução de CPI e privilégios

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árioPermitido?Ponto de aplicaçãoErro
Chamador passa conta como gravável, programa chamado marca como gravávelSim----
Chamador passa conta como somente leitura, programa chamado marca como gravávelNãoprepare_next_instructionPrivilegeEscalation
Chamador passa conta como gravável, programa chamado marca como somente leituraSim----
Chamador passa conta como signatário, programa chamado marca como signatárioSim----
Chamador passa conta como não signatário, programa chamado marca como signatário, conta é um PDA derivado das seeds do chamadorSimprepare_next_instruction--
Chamador passa conta como não signatário, programa chamado marca como signatário, conta NÃO é um PDA do chamadorNãoprepare_next_instructionPrivilegeEscalation
Chamador passa conta como signatário, programa chamado marca como não signatárioSim----
Programa A chama a si mesmo diretamente (A -> A)Simpush()--
Programa A chama B que chama A (reentrância indireta)Nãopush()ReentrancyNotAllowed
CPI para native loader, bpf_loader, bpf_loader_deprecated ou precompileNãocheck_authorized_programProgramNotSupported
Conta não encontrada na transaçãoNãoprepare_next_instructionMissingAccount

As regras de privilégio podem ser resumidas como:

  1. 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.
  2. 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.
  3. 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:

  1. Verifica o número de conjuntos de seeds de assinante contra MAX_SIGNERS (16).
  2. Verifica o comprimento de cada conjunto de seeds contra MAX_SEEDS (16 seeds por conjunto).
  3. Chama Pubkey::create_program_address com as seeds e o ID do programa do chamador. Se as seeds não produzirem um PDA válido, o CPI falha com BadSeeds.
  4. 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_loader e bpf_loader_deprecated
  • bpf_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:

  1. Valida a contagem de informações da conta contra MAX_CPI_ACCOUNT_INFOS (128, ou 255 com SIMD-0339).
  2. Cobra o custo de tradução de informações da conta (apenas SIMD-0339): (num_account_infos * 80) / 250 CUs.
  3. Para cada conta não executável e não duplicada, constrói um CallerAccount traduzindo 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_unit CUs.

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:

  1. 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 retorna ReentrancyNotAllowed.
  2. Chama process_executable_chain que 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.
  3. Chama pop() para remover o frame do chamado e verificar se os saldos de lamport permanecem inalterados (UnbalancedInstruction caso 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?

Gerenciado por

© 2026 Fundação Solana.
Todos os direitos reservados.
Conecte-se
  • Blog