Выполнение CPI и привилегии

Кратко

CPI проходят через 11 этапов рантайма, включая проверку привилегий, трансляцию аккаунтов и синхронизацию данных. Максимальная глубина вызова: 5 (9 с SIMD-0268). Правила привилегий не позволяют вызываемой программе получить больше прав, чем предоставил вызывающий.

Правила привилегий

CPI расширяют привилегии аккаунтов вызывающего для вызываемой стороны с жёстким контролем. Рантайм проверяет эти правила в prepare_next_instruction:

СценарийРазрешено?Точка контроляОшибка
Вызывающий передаёт аккаунт как writable, вызываемый помечает writableДа----
Вызывающий передаёт аккаунт как read-only, вызываемый помечает writableНетprepare_next_instructionPrivilegeEscalation
Вызывающий передаёт аккаунт как writable, вызываемый помечает read-onlyДа----
Вызывающий передаёт аккаунт как signer, вызываемый помечает signerДа----
Вызывающий передаёт аккаунт как non-signer, вызываемый помечает signer, аккаунт — PDA, полученный из seeds вызывающегоДаprepare_next_instruction--
Вызывающий передаёт аккаунт как non-signer, вызываемый помечает signer, аккаунт НЕ является PDA от вызывающегоНетprepare_next_instructionPrivilegeEscalation
Вызывающий передаёт аккаунт как signer, вызываемый помечает non-signerДа----
Программа A вызывает саму себя напрямую (A -> A)Даpush()--
Программа A вызывает B, которая вызывает A (косвенная реентерабельность)Нетpush()ReentrancyNotAllowed
CPI к native loader, bpf_loader, bpf_loader_deprecated или precompileНетcheck_authorized_programProgramNotSupported
Аккаунт не найден в транзакцииНетprepare_next_instructionMissingAccount

Правила привилегий можно резюмировать так:

  1. Привилегия записи не может быть повышена. Если вызывающий отмечает аккаунт как только для чтения, вызываемый не может сделать его доступным для записи.
  2. Привилегия подписи требует авторизации. Аккаунт может быть подписантом у вызываемого только если (а) он уже был подписантом у вызывающего, ИЛИ (б) это PDA, полученный из сидов вызывающей программы через invoke_signed.
  3. Снижение привилегий всегда разрешено. Вызываемый может использовать меньше привилегий, чем предоставил вызывающий.

Последовательность выполнения CPI

CPI проходит через несколько уровней среды выполнения. В этом разделе описан полный процесс — от вызова SDK программы до границы системного вызова, затем в рантайм и обратно. Каждый шаг ссылается на исходный файл, который его реализует.

Максимальная высота вложенности вызова инструкции программы называется max_instruction_stack_depth и задаётся константой MAX_INSTRUCTION_STACK_DEPTH со значением 5. При активном MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 это значение увеличивается до 9.

Высота стека 1 — это начальная инструкция транзакции. Каждый CPI увеличивает высоту на 1. Максимум 5 означает, что программа может делать вложенные CPI до 4 уровней (до 8 уровней с SIMD-0268).

Шаг 1: Программа вызывает invoke или invoke_signed

Программа вызывает invoke или invoke_signed. invoke — это тонкая обёртка, которая вызывает invoke_signed с пустым массивом сидов подписанта. Функция SDK сериализует Instruction, AccountInfo слайс и сиды подписанта в память VM, затем инициирует системный вызов.

Шаг 2: Вход в системный вызов

SBF VM передаёт управление обработчику системного вызова sol_invoke_signed_rust, который вызывает общий входной пункт: cpi_common.

Шаг 3: Учёт стоимости вызова

Первое действие внутри cpi_common — это списание фиксированной стоимости вызова с общего счётчика вычислений: invoke_units = 1 000 CU (или 946 CU с SIMD-0339).

Шаг 4: Трансляция инструкции из памяти VM

Обработчик системного вызова преобразует инструкцию из адресного пространства VM программы в типы Rust на стороне хоста через translate_instruction_rust, который читает структуру StableInstruction, проверяет длину данных с помощью MAX_INSTRUCTION_DATA_LEN (10 240 байт), затем списывает стоимость сериализации данных.

Шаг 5: Трансляция signer seeds и вывод PDA

Обработчик вызывает translate_signers_rust. Для каждого набора signer seeds рантайм выполняет:

  1. Проверяет количество наборов signer seeds с помощью MAX_SIGNERS (16).
  2. Проверяет длину каждого набора seed с помощью MAX_SEEDS (16 seeds в наборе).
  3. Вызывает Pubkey::create_program_address с seeds и программным ID вызывающего. Если seeds не дают валидный PDA, CPI завершается с ошибкой BadSeeds.
  4. Собирает полученные PDA pubkey в вектор signers.

Эти полученные PDA считаются валидными подписантами для инструкции вызываемой программы.

Шаг 6: Проверка авторизованной программы

Перед продолжением рантайм вызывает check_authorized_program, чтобы убедиться, что целевая программа разрешена для CPI. Следующие программы блокируются:

  • Встроенный загрузчик
  • bpf_loader и bpf_loader_deprecated
  • bpf_loader_upgradeable (кроме отдельных управляющих инструкций: upgrade, set_authority, set_authority_checked (с фичей), extend_program_checked (с фичей), close)
  • Прекомпилированные программы (ed25519, secp256k1 и др.)

В случае нарушения возвращается ProgramNotSupported.

Шаг 7: Проверка привилегий (prepare_next_instruction)

Рантайм вызывает prepare_next_instruction, который формирует список InstructionAccount вызываемой стороны и применяет правила привилегий. Полная таблица решений приведена в разделе Правила привилегий ниже.

Шаг 8: Перевод информации об аккаунтах

Обработчик вызывает translate_accounts, который:

  1. Проверяет количество информации об аккаунтах относительно MAX_CPI_ACCOUNT_INFOS (128 или 255 с SIMD-0339).
  2. Взимает стоимость перевода информации об аккаунтах (только SIMD-0339): (num_account_infos * 80) / 250 CU.
  3. Для каждого неисполняемого и не дублирующегося аккаунта формирует CallerAccount, переводя указатели из памяти ВМ в память хоста. Это включает взимание стоимости сериализации данных аккаунта: account_data_len / cpi_bytes_per_unit CU.

Шаг 9: Пред-CPI синхронизация аккаунтов (от вызывающего к вызываемому)

Перед выполнением вызываемого, рантайм синхронизирует изменения аккаунта вызывающего, чтобы вызываемый мог их видеть. Функция update_callee_account вызывается для каждого переведённого аккаунта, копируя lamports, данные и владельца. Подробнее о сопоставлении полей см. Синхронизация данных аккаунта.

Шаг 10: Поместить контекст инструкции, выполнить вызываемого и удалить из стека

Рантайм вызывает process_instruction, который:

  1. Вызывает push() для добавления нового фрейма в стек инструкций. push() обеспечивает правило реентерабельности: программа может вызвать саму себя только если она является прямым вызывающим (т.е. программа A может вызвать A, но A не может вызвать B, который вызывает A). При нарушении возвращается ReentrancyNotAllowed.
  2. Вызывает process_executable_chain, который определяет точку входа программы вызываемого и запускает её. Вызываемый работает с тем же общим счётчиком вычислений. Все CU, потраченные вызываемым, уменьшают оставшийся бюджет вызывающего.
  3. Вызывает pop() для удаления фрейма вызываемого и проверки, что балансы lamport не изменились (UnbalancedInstruction, если изменились).

Шаг 11: Пост-CPI синхронизация аккаунтов (от вызываемого к вызывающему)

После возврата process_instruction (включая pop), рантайм синхронизирует изменения обратно к вызывающему через update_caller_account для каждого доступного для записи аккаунта. Дополнительно, update_caller_account_region обновляет отображение регионов памяти ВМ для аккаунтов, чьи области данных изменились. Подробнее о сопоставлении полей см. Синхронизация данных аккаунта.

Системный вызов CPI возвращает 0 (успех) вызывающей программе.

Is this page helpful?

Управляется

© 2026 Solana Foundation.
Все права защищены.
Связаться с нами