Кратко
CPI проходят через 11 этапов рантайма, включая проверку привилегий, трансляцию аккаунтов и синхронизацию данных. Максимальная глубина вызова: 5 (9 с SIMD-0268). Правила привилегий не позволяют вызываемой программе получить больше прав, чем предоставил вызывающий.
Правила привилегий
CPI расширяют привилегии аккаунтов вызывающего для вызываемой стороны с жёстким
контролем. Рантайм проверяет эти правила в
prepare_next_instruction:
| Сценарий | Разрешено? | Точка контроля | Ошибка |
|---|---|---|---|
| Вызывающий передаёт аккаунт как writable, вызываемый помечает writable | Да | -- | -- |
| Вызывающий передаёт аккаунт как read-only, вызываемый помечает writable | Нет | prepare_next_instruction | PrivilegeEscalation |
| Вызывающий передаёт аккаунт как writable, вызываемый помечает read-only | Да | -- | -- |
| Вызывающий передаёт аккаунт как signer, вызываемый помечает signer | Да | -- | -- |
| Вызывающий передаёт аккаунт как non-signer, вызываемый помечает signer, аккаунт — PDA, полученный из seeds вызывающего | Да | prepare_next_instruction | -- |
| Вызывающий передаёт аккаунт как non-signer, вызываемый помечает signer, аккаунт НЕ является PDA от вызывающего | Нет | prepare_next_instruction | PrivilegeEscalation |
| Вызывающий передаёт аккаунт как signer, вызываемый помечает non-signer | Да | -- | -- |
| Программа A вызывает саму себя напрямую (A -> A) | Да | push() | -- |
| Программа A вызывает B, которая вызывает A (косвенная реентерабельность) | Нет | push() | ReentrancyNotAllowed |
| CPI к native loader, bpf_loader, bpf_loader_deprecated или precompile | Нет | check_authorized_program | ProgramNotSupported |
| Аккаунт не найден в транзакции | Нет | prepare_next_instruction | MissingAccount |
Правила привилегий можно резюмировать так:
- Привилегия записи не может быть повышена. Если вызывающий отмечает аккаунт как только для чтения, вызываемый не может сделать его доступным для записи.
- Привилегия подписи требует авторизации. Аккаунт может быть подписантом у
вызываемого только если (а) он уже был подписантом у вызывающего, ИЛИ (б) это
PDA, полученный из сидов вызывающей программы через
invoke_signed. - Снижение привилегий всегда разрешено. Вызываемый может использовать меньше привилегий, чем предоставил вызывающий.
Последовательность выполнения 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 рантайм выполняет:
- Проверяет количество наборов signer seeds с помощью
MAX_SIGNERS(16). - Проверяет длину каждого набора seed с помощью
MAX_SEEDS(16 seeds в наборе). - Вызывает
Pubkey::create_program_addressс seeds и программным ID вызывающего. Если seeds не дают валидный PDA, CPI завершается с ошибкойBadSeeds. - Собирает полученные PDA pubkey в вектор
signers.
Эти полученные PDA считаются валидными подписантами для инструкции вызываемой программы.
Шаг 6: Проверка авторизованной программы
Перед продолжением рантайм вызывает
check_authorized_program,
чтобы убедиться, что целевая программа разрешена для CPI. Следующие программы
блокируются:
- Встроенный загрузчик
bpf_loaderиbpf_loader_deprecatedbpf_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, который:
- Проверяет количество информации об аккаунтах
относительно
MAX_CPI_ACCOUNT_INFOS(128 или 255 с SIMD-0339). - Взимает стоимость перевода информации об аккаунтах
(только SIMD-0339):
(num_account_infos * 80) / 250CU. - Для каждого неисполняемого и не дублирующегося аккаунта формирует
CallerAccount, переводя указатели из памяти ВМ в память хоста. Это включает взимание стоимости сериализации данных аккаунта:account_data_len / cpi_bytes_per_unitCU.
Шаг 9: Пред-CPI синхронизация аккаунтов (от вызывающего к вызываемому)
Перед выполнением вызываемого, рантайм синхронизирует изменения аккаунта
вызывающего, чтобы вызываемый мог их видеть. Функция
update_callee_account
вызывается для каждого переведённого аккаунта, копируя lamports, данные и
владельца. Подробнее о сопоставлении полей см.
Синхронизация данных аккаунта.
Шаг 10: Поместить контекст инструкции, выполнить вызываемого и удалить из стека
Рантайм вызывает
process_instruction,
который:
- Вызывает
push()для добавления нового фрейма в стек инструкций.push()обеспечивает правило реентерабельности: программа может вызвать саму себя только если она является прямым вызывающим (т.е. программа A может вызвать A, но A не может вызвать B, который вызывает A). При нарушении возвращаетсяReentrancyNotAllowed. - Вызывает
process_executable_chain, который определяет точку входа программы вызываемого и запускает её. Вызываемый работает с тем же общим счётчиком вычислений. Все CU, потраченные вызываемым, уменьшают оставшийся бюджет вызывающего. - Вызывает
pop()для удаления фрейма вызываемого и проверки, что балансы lamport не изменились (UnbalancedInstruction, если изменились).
Шаг 11: Пост-CPI синхронизация аккаунтов (от вызываемого к вызывающему)
После возврата process_instruction (включая pop), рантайм синхронизирует
изменения обратно к вызывающему через
update_caller_account
для каждого доступного для записи аккаунта. Дополнительно,
update_caller_account_region
обновляет отображение регионов памяти ВМ для аккаунтов, чьи области данных
изменились. Подробнее о сопоставлении полей см.
Синхронизация данных аккаунта.
Системный вызов CPI возвращает 0 (успех) вызывающей программе.
Is this page helpful?