НОВОЕ: Helius приобретает Light Protocol
создание смарт-контрактов Solana с помощью Pinocchio
Блог/Разработка

Как создавать программы Solana с помощью Pinocchio

Ведущее агентство по разработке на SolanaExo Technologies в XExo Technologies в LinkedIn
Соучредитель, Exo TechnologiesTaylor Johnson в XTaylor Johnson в LinkedIn
Чтение: 12 мин

Pinocchio — это высокооптимизированная библиотека без зависимостей, которую можно использовать для создания нативных программ Solana. Pinocchio разработала Anza — команда основных разработчиков клиента Agave для Solana. 

Exo Tech — ведущая компания по разработке на Solana и один из первых пользователей Pinocchio. Работая над клиентскими проектами, мы создали с помощью Pinocchio несколько программ для продакшена и внесли вклад в SDK, добавив недостающие функции. 

В этой статье мы подробно рассмотрим создание программ с помощью Pinocchio, а также его преимущества и компромиссы. Наша цель — дать разработчикам знания, которые помогут определить, подходит ли Pinocchio для их программы. Однако важно учитывать, что Pinocchio не подходит начинающим, поскольку оптимизация для него важнее удобства разработки.

Что такое библиотека Pinocchio?

Библиотека Pinocchio заменяет крейт solana-program и оптимизирует выполнение программ благодаря активному использованию типов zero-copy. zero-copy означает, что при чтении или записи данные не нужно копировать по отдельному адресу памяти. Это экономит вычислительные ресурсы, или CU в Solana.

У библиотеки нет зависимостей, и она поддерживает режим “no_std”. Крейт Rust std предоставляет стандартные способы доступа к ресурсам операционной системы, а также среду выполнения. Но поскольку Solana Virtual Machine (SVM) сама является средой выполнения, эти накладные расходы не нужны.

Почему Pinocchio производительнее solana-program?

Каждой программе Solana нужна точка входа, которую среда выполнения вызывает для запуска программы. Библиотека solana-program предоставляет макрос entrypoint!, который десериализует входные данные программы, настраивает аллокатор кучи и создает обработчик паники.

Код
macro_rules! entrypoint {
    ($process_instruction:ident) => {
        /// # Safety
        #[no_mangle]
        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
            let (program_id, accounts, instruction_data) =
                unsafe { $crate::entrypoint::deserialize(input) };
            match $process_instruction(&program_id, &accounts, &instruction_data) {
                Ok(()) => $crate::entrypoint::SUCCESS,
                Err(error) => error.into(),
            }
        }
        $crate::custom_heap_default!();
        $crate::custom_panic_default!();
    };
}

Pinocchio экспортирует три макроса точки входа.

Для тех, кто переходит с solana-program, макрос entrypoint! будет работать практически так же: он десериализует входные данные программы и настраивает аллокатор и обработчик. 

Однако два других макроса отделяют точку входа от настройки аллокатора кучи и обработчика паники. Это дает разработчику больше контроля и позволяет исключить или оптимизировать эти операции до выполнения логики программы. 

program_entrypoint! десериализует входные данные программы аналогично solana-program, а lazy_program_entrypoint! просто оборачивает входной буфер и передает его обработку программе, обеспечивая больше контроля над вычислениями. 

Поскольку эти макросы не настраивают аллокатор кучи и обработчик паники, библиотека Pinocchio предоставляет разработчику стандартные макросы для этих задач.

Кроме того, если программе точно не потребуется память в куче, no_allocator! экономит вычислительные единицы (CU), не настраивая аллокатор памяти.

Чем отличается десериализация входных данных программ Solana в точках входа Pinocchio?

Мы кратко упомянули, что точки входа как solana-program, так и Pinocchio десериализуют входные данные программы. Важно понимать различия между этими способами десериализации, поскольку именно они обеспечивают значительную экономию CU. 

На первый взгляд десериализованные входные данные, передаваемые обработчику инструкции программы, выглядят одинаково:

Код
/// solana-program and pinocchio both look the same
process_instruction(
         program_id: &Pubkey,
         accounts: &[AccountInfo],
         instruction_data: &[u8],
     ) -> ProgramResult

Ключевое различие заключается в реализации AccountInfo. 

В то время как solana-program записывает данные в структуру AccountInfo, которая владеет ими, структура AccountInfo в Pinocchio сама по себе является лишь указателем на исходные входные данные, представляющие аккаунт. Это сокращает объем копируемых данных и значительно экономит CU.

Как Pinocchio помогает разработчикам оптимизировать CU?

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

Это легко заметить при попытке получить доступ к значениям в AccountInfo. Чтение публичного ключа аккаунта методом key() возвращает ссылку на Pubkey. Благодаря этому чтение информации об аккаунте во время выполнения программы и изменение его данных обходятся дешевле.

Пример оптимизации CU с Pinocchio: P-token

Отличный пример дальнейшего использования zero-copy для оптимизации — программа p-token.

Эта программа создана как замена канонической SPL Token Program, но использует Pinocchio, чтобы значительно сократить число вычислительных единиц для каждой транзакции.

Вы сразу заметите, что доступ ко всему состоянию осуществляется через указатели.

Вместо десериализации аккаунта токена проверяются данные из AccountInfo, после чего возвращается указатель.

Доступ к каждому свойству осуществляется через функцию, а все значения, не являющиеся примитивами, возвращают ссылку, сохраняя zero-copy. 

Подробнее о том, почему это значительно сокращает потребление CU, читайте в статье об оптимизации CU.

Pinocchio и Anchor

Anchor — популярный специализированный фреймворк для разработки программ Solana. Он считается более высокоуровневым, чем Pinocchio, поскольку не содержит логики для предоставления доступа к базовым структурам вроде AccountInfo.

Вместо этого Anchor зависит от упомянутого выше крейта solana-program и предоставляет трейты и макросы, упрощающие разработку программ. Anchor предоставляет шаблоны дискриминаторов инструкций и логику десериализации аккаунтов. Логика десериализации основана на Borsh, который не поддерживает zero-copy и поэтому требует копировать данные по другому адресу памяти. 

Удобство Anchor ускоряет разработку программ Solana, но приводит к повышенному потреблению CU.

Pinocchio, напротив, представляет собой библиотеку для замены solana-program в случаях, когда разработчикам нужен точный контроль над вычислительными ресурсами. Она не навязывает определенных подходов и позволяет разработчику организовать программу любым подходящим способом. Структура каждого проекта Pinocchio может быть совершенно разной, тогда как проекты Anchor имеют четко определенную структуру. 

Библиотека Pinocchio не предоставляет клиентские привязки и их реализацию. Anchor, напротив, имеет полноценную поддержку генерации IDL, который можно использовать на стороне клиента для взаимодействия с программой.

Разработчикам, использующим Pinocchio, необходимо писать их самостоятельно или использовать другие инструменты, например Shank и Codama. Мы рассматриваем их ниже, в разделе Дополнительные инструменты для разработки с Pinocchio.

Pinocchio и Steel

Steel — еще один фреймворк для написания программ Solana. Сейчас он построен поверх solana-program и предоставляет макросы, функции и шаблоны, упрощающие создание безопасных и выразительных программ.

Благодаря специализированному подходу код Steel легко читать, при этом он остается модульным. В отличие от Anchor, который используется по принципу «всё или ничего», Steel позволяет разработчикам выбирать только необходимые компоненты.

Макрос account! в Steel использует bytemuck для разбора структур аккаунтов, тогда как Pinocchio вообще не обрабатывает разбор аккаунтов. Steel также предоставляет цепочки парсеров и проверок, упрощая добавление пользовательской валидации. В Pinocchio нет готовых подобных шаблонов, поэтому разработчику придется написать их самостоятельно.

Однако для распространенных межпрограммных вызовов (CPI), например System Program и Token Program, как Pinocchio, так и Steel предоставляют шаблоны, упрощающие такие вызовы.

Pinocchio тщательно оптимизирован, но оставляет все детали на усмотрение разработчика. Steel — удобная модульная обертка над библиотекой solana-program, созданная для улучшения процесса разработки.

Как создать токен с помощью Pinocchio

Чтобы продемонстрировать программу на Pinocchio, мы перепишем программу создания токена из примеров для разработчиков Solana.

Это простая программа с одной инструкцией, которая создает минт токена Token2022 и использует расширение токена Metadata для хранения информации о токене. Метаданные передаются через данные инструкции, содержащие имя, символ и uri.

1. Определите точку входа

Начнем с определения точки входа нашей программы.

Мы используем полный макрос точки входа, поскольку хотим использовать стандартный аллокатор и обработчик паники Pinocchio.

Код
entrypoint!(process_instruction);

fn process_instruction(
   _program_id: &Pubkey,
   accounts: &[AccountInfo],
   instruction_data: &[u8],
) -> ProgramResult {
   Ok(())
}

2. Определите структуру данных инструкции

Затем определим структуру данных инструкции в соответствии с другими примерами программ. Чтобы сократить время разработки, мы используем Borsh для десериализации, а более оптимальные методы десериализации рассмотрим в другой статье.

Код
#[derive(BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
   pub name: String,
   pub symbol: String,
   pub uri: String,
   pub decimals: u8,
}

3. Разберите аккаунты и данные инструкции

Теперь напишем логику внутри обработчика инструкций.

Сначала необходимо деструктурировать аккаунты из списка аккаунтов и десериализовать данные инструкции в наш CreateTokenArgs.

Код
let [mint_account, mint_authority, payer, token_program, _system_program] = accounts else {
       return Err(ProgramError::NotEnoughAccountKeys);
   };

   let args = CreateTokenArgs::try_from_slice(instruction_data)
       .map_err(|_| ProgramError::InvalidInstructionData)?;

4. Создайте аккаунт минта Token2022

После разбора аккаунтов и данных инструкции мы вызываем инструкцию CreateAccount программы System.

Ниже мы используем структуру CreateAccount из `pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invoke.

В отличие от создания обычного минта SPL Token, нам необходимо определить дополнительный объем пространства для используемых расширений токена.

Размер расширения Metadata Pointer статичен, а размер расширения Token Metadata необходимо вычислять динамически на основе переданных аргументов.

Код
 /// [4 (extension discriminator) + 32 (update_authority) + 32 (metadata)]
   const METADATA_POINTER_SIZE: usize = 4 + 32 + 32;
   /// [4 (extension discriminator) + 32 (update_authority) + 32 (mint) + 4 (size of name ) + 4 (size of symbol) + 4 (size of uri) + 4 (size of additional_metadata)]
   const METADATA_EXTENSION_BASE_SIZE: usize = 4 + 32 + 32 + 4 + 4 + 4 + 4;
   /// Padding used so that Mint and Account extensions start at the same index
   const EXTENSIONS_PADDING_AND_OFFSET: usize = 84;

   /* within `process_instruction` */
   let extension_size = METADATA_POINTER_SIZE
       + METADATA_EXTENSION_BASE_SIZE
       + args.name.len()
       + args.symbol.len()
       + args.uri.len();
   let total_mint_size = Mint::LEN + EXTENSIONS_PADDING_AND_OFFSET + extension_size;

   let rent = Rent::get()?;
   // Create the account for the Mint
   CreateAccount {
       from: payer,
       to: mint_account,
       owner: token2022_program.key(),
       lamports: rent.minimum_balance(Mint::LEN),
       space: Mint::LEN as u64,
   }
   .invoke()?;

После вызова CreateAccount SystemProgram регистрирует программу Token2022 как владельца аккаунта минта.

5. Инициализируйте расширение, аккаунт и значения метаданных

Затем необходимо задать данные аккаунта: инициализировать расширение Metadata Pointer, инициализировать аккаунт Mint с помощью программы Token2022 и инициализировать значения метаданных, полученные нашей программой в качестве аргументов. 

Следующие CPI взяты из активно разрабатываемой ветки крейта pinocchio_token. Стоит учитывать, что этот код, скорее всего, устареет, поскольку функциональность Token2022 планируется отделить от крейта SPL Token.

Код
// Initialize MetadataPointer extension pointing to the Mint account
   InitializeMetadataPointer {
       mint: mint_account,
       authority: Some(*payer.key()),
       metadata_address: Some(*mint_account.key()),
   }
   .invoke()?;

   // Now initialize that account as a Token2022 Mint
   InitializeMint2 {
       mint: mint_account,
       decimals: args.decimals,
       mint_authority: mint_authority.key(),
       freeze_authority: None,
   }
   .invoke(TokenProgramVariant::Token2022)?;

   // Set the metadata within the Mint account
   InitializeTokenMetadata {
       metadata: mint_account,
       update_authority: payer,
       mint: mint_account,
       mint_authority: payer,
       name: &args.name,
       symbol: &args.symbol,
       uri: &args.uri,
   }
   .invoke()?;

Готово! 

Теперь у нас есть минт токена с автономными метаданными на основе Token2022, созданный с помощью Pinocchio.

Этот код можно улучшить для максимальной оптимизации, но мы надеемся, что он поможет вам понять, как писать программы с помощью Pinocchio.

Дополнительные инструменты для разработки с Pinocchio

Специализированных инструментов для Pinocchio пока немного, но их число растет.

Bytemuck для (де)сериализации аккаунтов

Разработчик программы Pinocchio должен самостоятельно реализовать (де)сериализацию аккаунтов. При ручной реализации это утомительный процесс, чреватый ошибками. Bytemuck — отличная библиотека, которая упрощает чтение и запись массивов байтов в виде структур. Она достаточно хорошо оптимизирована, поскольку ограничивает объем данных, которые нужно копировать в память.

Borsh — еще одно решение для работы с аккаунтами, не имеющими фиксированного размера. Однако оно менее эффективно с точки зрения вычислительных ресурсов, и это одна из причин, по которым выбирают Pinocchio вместо Anchor.

Shank для генерации IDL

Поскольку Pinocchio — это библиотека, в ней нет встроенной генерации IDL, как в Anchor. IDL (Interface Definition Language) — это JSON-файл, определяющий публичный интерфейс программы Solana, включая ее инструкции, структуры аккаунтов и коды ошибок. Он стандартизирует взаимодействие и упрощает разработку на стороне клиента.

Для генерации IDL мы рекомендуем использовать Shank. Этот крейт позволяет разработчикам очень легко аннотировать код и генерировать корректный IDL с помощью CLI. Добавление макроса ShankAccount в выражение derive структуры указывает, что это аккаунт, который должен поддерживать (де)сериализацию. После запуска CLI shank эта структура появится в IDL как типизированный аккаунт, который затем можно использовать для генерации клиента.

Еще один важный макрос — ShankInstruction для перечисления инструкций программы. Он позволяет использовать атрибут #[account], чтобы указать индекс и разрешения каждого аккаунта в списке для конкретной инструкции.

Дополнительную информацию о полезных аннотациях кода, упрощающих генерацию IDL для программ без Anchor, см. в репозитории shank-macro.

Codama для генерации клиентов

После получения IDL клиент можно легко сгенерировать с помощью Codama. Если сгенерированный код не соответствует вашим требованиям, придется написать клиенты вручную.

В Exo Tech мы создали шаблон проекта Pinocchio, который помогает нам быстро создавать репозитории программ Solana. Попробуйте его и отправляйте pull request с любыми улучшениями!

Будущее Pinocchio

Хотя Pinocchio задуман как прямая замена solana-program, функциональность пока не достигла полного соответствия. Некоторые sysvar еще не поддерживаются, а поддержка неосновных крейтов либо неполна, либо вовсе отсутствует. Например, крейт Pinocchio Token Program не поддерживает несколько подписантов. Поддержки Token2022 также пока нет, но она находится в разработке.

Один из наиболее существенных недостатков Pinocchio заключается в том, что все SDK, разработанные для других программ Solana, используют крейт solana-program. Это означает, что каждому SDK требуется владение AccountInfo или передаваемыми данными, из-за чего обеспечить совместимость с программой на Pinocchio крайне сложно. 

При интеграции со сторонними программами часто приходится писать отдельную логику CPI для каждой инструкции. В будущем эту проблему могут решить генераторы кода вроде Codama, но пока они к этому не готовы.

Важно учитывать, что Pinocchio все еще активно разрабатывается и не проходил аудит. Сообщество продолжает добавлять в SDK остальные sysvar и улучшать поддержку важных программ SPL, таких как Token и Token2022.

Как внести вклад в Pinocchio

В Pinocchio есть множество простых задач, с которых можно начать участие в разработке.

Есть открытые задачи и существующие pull request, которым не помешает дополнительная помощь. Присоединяйтесь к обсуждениям или просто создайте pull request, чтобы сопровождающие могли его проверить!

Заключение

Pinocchio — значительно более производительная библиотека для написания программ Solana по сравнению с предыдущими решениями. Большая гибкость при настройке точки входа программы и использование zero-copy для доступа к входным данным помогают разработчикам сократить потребление CU. Однако библиотека все еще новая, и ее функциональность пока неполна. На момент написания статьи библиотека не прошла аудит, поэтому используйте ее с осторожностью.

При принятии решения об использовании Pinocchio важно оценить компромиссы по сравнению с другими библиотеками и фреймворками.

Специализированные фреймворки вроде Anchor ускоряют разработку программ и упрощают их сопровождение. Это отличный выбор, когда важно быстро вывести продукт на рынок.

Когда ваш продукт стабилизируется и начнет обрабатывать большой объем транзакций, оптимизация программ Solana с помощью такой библиотеки, как Pinocchio, может оказаться более подходящим решением.

Дополнительные ресурсы

Чтобы узнать больше, посмотрите выступление Febo на Solana Accelerate 2025 и изучите эти образовательные материалы:

Подпишитесь на Helius

Следите за новостями разработки на Solana и получайте уведомления о наших публикациях

Увеличенное изображение