Версії для прикладів у «Що таке CPI у Solana» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.

CPI (Cross-Program Invocation) — це механізм, який дозволяє одній програмі на Solana викликати іншу програму під час виконання транзакції. Без CPI кожна програма була б ізольованою: вона могла б працювати лише з власними даними. CPI знімає цю ізоляцію й утворює основу для будь-якої нетривіальної логіки в екосистемі — від переказу токенів до взаємодії з децентралізованими біржами.

Означення CPI

Cross-Program Invocation — виклик однією програмою іншою

CPI — це процес, під час якого виконувана програма передає керування іншій програмі, передаючи їй набір акаунтів і інструкцію для обробки. Після завершення виклику керування повертається до початкової програми. Уся дія відбувається в межах однієї транзакції: немає окремих мережевих запитів, немає додаткових підписів користувача.

Ключова властивість CPI: права доступу (signer, writable) передаються від батьківської програми до дочірньої, але з важливими винятками, про які нижче.

Аналогія з викликом функції між модулями

У традиційній розробці на Rust ви імпортуєте модуль і викликаєте його функцію. У Solana програми не можуть імпортувати одна одну на рівні байт-коду — кожна програма є окремим розгорнутим контрактом. CPI відіграє роль такого «виклику функції», але через інтерфейс виконувача (runtime) Solana: ви вказуєте адресу програми, список акаунтів і серіалізовані дані інструкції.

Різниця у тому, що виклик між модулями відбувається в межах одного процесу, а CPI — це формалізований перехід керування між різними програмами з перевіркою прав на рівні runtime.

Як працює CPI

invoke та invoke_signed

У Solana існують дві базові функції для CPI, які доступні через crate solana-program:

invoke — використовується, коли всі необхідні підписи вже присутні в транзакції (наприклад, користувач підписав транзакцію, і програма передає цей підпис далі).

invoke_signed — використовується, коли програма має надати підпис від імені PDA (Program Derived Address). PDA не має приватного ключа, тому підпис генерується програмно через набір seed-ів.

Нижче наведено концептуальний приклад виклику System Program для створення акаунта. Це не production-рішення, а ілюстрація того, як виглядає структура виклику.

Середовище: Anchor 0.30.1, Solana CLI 1.18.26, кластер Devnet.

solana_program::program::invoke(
    &instruction, // інструкція для цільової програми
    &account_infos, // масив акаунтів, які передаються
)?;

У фреймворку Anchor ці виклики абстрагуються через моделі акаунтів та контексти (CPI context), але під капотом відбувається саме виклик invoke або invoke_signed.

Передача акаунтів та даних між програмами

Під час CPI програма формує два об'єкти:

  • Інструкція — містить ідентифікатор програми (pubkey), масив метаданих акаунтів (які саме акаунти передаються і які права на них є) та серіалізовані дані інструкції (байти, які цільова програма розпізнає як конкретну команду).
  • Масив акаунтів — посилання на об'єкти AccountInfo, які вже пройшли перевірку в батьківській програмі.

Runtime Solana перевіряє, що кожен акаунт у масиві відповідає метаданим у інструкції: якщо акаунт позначений як writable у інструкції, він має бути writable і в транзакції. Ця перевірка запобігає підвищенню привілеїв під час CPI.

Обмеження CPI

Глибина рекурсії (4 рівні)

Solana обмежує глибину вкладених викликів CPI чотирма рівнями. Лічильник починається з нуля для першої програми в транзакції, тобто максимально допустимий ланцюжок виглядає так:

  1. Програма A (рівень 0) викликає Програму B
  2. Програма B (рівень 1) викликає Програму C
  3. Програма C (рівень 2) викликає Програму D
  4. Програма D (рівень 3) викликає Програму E

Якщо Програма E спробує зробити CPI, runtime поверне помилку InstructionError::CallDepth. Це обмеження існує для запобігання нескінченній рекурсії та перевантаженню валідаторів.

Compute units: кожен CPI додає витрати

Кожен виклик CPI споживає compute units на два компоненти:

  • Накладні витрати самого виклику — серіалізація інструкції, перевірка прав акаунтів, передача керування.
  • Виконання цільової програми — логіка програми, до якої ви звертаєтесь.

Стандартний ліміт на транзакцію — 200 000 compute units (можна збільшити до 1 400 000 з приоритетною комісією). Глибокі ланцюжки CPI з кількома програмами можуть швидко вичерпати цей ліміт. Під час розробки на Devnet завжди перевіряйте спожиті compute units через логи транзакції (solana confirm -v), щоб уникнути несподіваних помилок на Mainnet.

Типові сценарії CPI

Виклик System Program (створення акаунта)

Найчастіший сценарій — програма створює новий акаунт для зберігання даних. Для цього вона викликає System Program через CPI з інструкцією create_account, передаючи PDA як signera (через invoke_signed).

Це концептуальна структура того, що відбувається:

  • Програма обчислює PDA для нового акаунта.
  • Формує інструкцію create_account для System Program (вказує розмір, ламports, власника).
  • Викликає invoke_signed, передаючи PDA-підпис.
  • Після успішного CPI програма може записувати дані в створений акаунт.

Детальний приклад коду для цього сценарію розглядається в розділі Як створити PDA у програмі.

Виклик Token Program (transfer, mint)

Щоб програма могла переказати токени (SPL) або здійснити mint, вона викликає Token Program через CPI. Типові інструкції:

  • transfer — переказ токенів між акаунтами. Програма передає акаунти відправника та отримувача, а також токен-акаунти.
  • mint_to — емісія нових токенів на вказаний токен-акаунт. Викликається з PDA-підписом mint-авторитету.
  • burn — знищення токенів.

У всіх цих випадках програма не зберігає токени сама по собі — вона делегує роботу з токен-акаунтами Token Program через CPI.

Типові помилки

CPI caller is not signer

Помилка InstructionError::InvalidAccountData або NotEnoughSigners виникає, коли цільова програма очікує підпис акаунта, але батьківська програма передала його без прав signer.

Причина: під час звичайного invoke права signer не створюються з нуля — вони лише передаються з оригінальної транзакції. Якщо користувач не підписав цей акаунт у транзакції, програма не може «надати» йому статус signer через invoke.

Рішення: якщо акаунт є PDA і програма має право його підписувати, використовуйте invoke_signed з правильними seed-ами. Якщо акаунт належить користувачу — він має підписати транзакцію.

Невірний порядок акаунтів у CPI

Кожна програма на Solana очікує акаунти у суворому порядку, визначеному її інтерфейсом. Якщо ви передасте акаунти в іншому порядку під час CPI, цільова програма інтерпретує їх неправильно: наприклад, токен-акаунт відправника буде прочитаний як токен-акаунт отримувача.

Це особливо небезпечно, тому що runtime не перевіряє семантичний зміст акаунтів — лише їхні права (writable, signer) та відповідність метаданим інструкції. Транзакція може пройти валідацію, але логіка програми спрацює некоректно.

Рішення: завжди перевіряйте порядок акаунтів у документації цільової програми (або в її IDL, якщо це Anchor-програма) і строго дотримуйтесь його при формуванні інструкції для CPI. У Anchor контексти (CpiContext) частково захищають від цієї помилки, оскільки порядок задається на рівні типів, але ручне формування інструкцій вимагає особливої уваги.

Практичні приклади формування CPI-викликів з коректним порядком акаунтів та обробкою помилок розглядаються в наступному розділі: Як викликати іншу програму через CPI.

Джерела