MỚI: Helius mua lại Light Protocol
xây dựng hợp đồng thông minh Solana với Pinocchio
Blog/Phát triển

Cách xây dựng chương trình Solana với Pinocchio

Đơn vị phát triển Solana hàng đầuExo Technologies trên XExo Technologies trên LinkedIn
Đồng sáng lập, Exo TechnologiesTaylor Johnson trên XTaylor Johnson trên LinkedIn
Đọc trong 12 phút

Pinocchio là một thư viện không phụ thuộc được tối ưu hóa cao, có thể dùng để xây dựng các chương trình Solana nguyên bản. Pinocchio do Anza, đội ngũ phát triển cốt lõi của client Agave cho Solana, tạo ra. 

Exo Tech là một đơn vị phát triển Solana hàng đầu và cũng là một trong những bên đầu tiên áp dụng Pinocchio. Thông qua công việc cho khách hàng, chúng tôi đã phát triển nhiều chương trình production bằng Pinocchio và đóng góp vào SDK để bổ sung các chức năng còn thiếu. 

Bài viết này phân tích chuyên sâu cách xây dựng chương trình với Pinocchio, đồng thời xem xét các lợi ích và điểm đánh đổi. Mục tiêu là trang bị cho nhà phát triển kiến thức để xác định Pinocchio có phù hợp với chương trình của họ hay không. Tuy nhiên, cần lưu ý rằng Pinocchio không thân thiện với người mới bắt đầu vì ưu tiên tối ưu hóa hơn trải nghiệm nhà phát triển.

Thư viện Pinocchio là gì?

Thư viện Pinocchio thay thế crate solana-program và tối ưu hóa việc thực thi chương trình bằng cách sử dụng rộng rãi các kiểu zero-copy. zero-copy có nghĩa là dữ liệu không cần được sao chép sang một địa chỉ bộ nhớ riêng khi đọc hoặc ghi, nhờ đó tiết kiệm tài nguyên tính toán (hay CU trong Solana).

Thư viện này không có dependency và là “no_std”. Crate std của Rust cung cấp các phương thức phổ biến để truy cập tài nguyên hệ điều hành cũng như một runtime. Tuy nhiên, vì Solana Virtual Machine (SVM) bản thân đã là một runtime nên phần chi phí bổ sung này không cần thiết.

Vì sao Pinocchio có hiệu năng cao hơn solana-program?

Mọi chương trình Solana đều cần một entrypoint để runtime gọi khi thực thi chương trình. Thư viện solana-program cung cấp macro entrypoint!. Macro này giải tuần tự hóa đầu vào của chương trình, thiết lập heap allocator và tạo panic handler.

Mã
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 cung cấp ba macro entrypoint.

Đối với những ai chuyển từ solana-program, macro entrypoint! hoạt động gần như tương tự: giải tuần tự hóa đầu vào của chương trình, đồng thời thiết lập allocator và handler. 

Tuy nhiên, hai macro còn lại tách entrypoint khỏi quá trình thiết lập heap allocator và panic handler, giúp nhà phát triển có thêm quyền kiểm soát để lược bỏ hoặc tối ưu trước khi logic chương trình được thực thi. 

program_entrypoint! giải tuần tự hóa đầu vào chương trình tương tự solana-program, còn lazy_program_entrypoint! chỉ bao bọc buffer đầu vào và để chương trình xử lý sau, qua đó cho phép kiểm soát hoạt động tính toán tốt hơn. 

Vì các macro này không thiết lập heap allocator hoặc panic handler, thư viện Pinocchio cung cấp các macro mặc định để nhà phát triển sử dụng.

Ngoài ra, nếu một chương trình biết chắc rằng nó sẽ không bao giờ cần bộ nhớ heap, no_allocator! sẽ tiết kiệm đơn vị tính toán (CU) bằng cách bỏ qua việc thiết lập memory allocator.

Các entrypoint của Pinocchio giải tuần tự hóa đầu vào chương trình Solana khác biệt ra sao?

Chúng ta đã đề cập ngắn gọn cách entrypoint của cả solana-program và Pinocchio giải tuần tự hóa đầu vào chương trình. Tuy nhiên, cần hiểu rõ sự khác biệt giữa hai cách giải tuần tự hóa vì đây là nguồn gốc của phần CU tiết kiệm được đáng kể. 

Thoạt nhìn, các đầu vào đã giải tuần tự hóa được truyền tới instruction handler của chương trình trông giống nhau:

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

Khác biệt chính nằm ở cách triển khai AccountInfo. 

Trong khi solana-program ghi dữ liệu vào một struct AccountInfo sở hữu dữ liệu đó, struct AccountInfo của Pinocchio bản thân chỉ là một con trỏ tới dữ liệu đầu vào nền đại diện cho tài khoản. Cách này giảm lượng dữ liệu cần sao chép và tiết kiệm rất nhiều CU.

Pinocchio giúp nhà phát triển tối ưu CU như thế nào?

Vì instruction processor nhận các tham chiếu tới con trỏ, nhà phát triển dùng thư viện Pinocchio sẽ nhận thấy logic của họ hiếm khi sở hữu dữ liệu đang được xử lý. 

Điều này dễ nhận thấy khi cố truy cập các giá trị trên AccountInfo. Việc đọc khóa công khai của tài khoản bằng phương thức key() sẽ trả về một tham chiếu tới Pubkey. Nhờ đó, chi phí đọc thông tin tài khoản trong suốt quá trình thực thi chương trình và chi phí sửa đổi dữ liệu tài khoản đều thấp hơn.

Ví dụ tối ưu CU bằng Pinocchio: P-token

Một ví dụ tiêu biểu tiếp tục tận dụng zero-copy để tối ưu là chương trình p-token.

Chương trình này được viết để thay thế SPL Token Program chuẩn, nhưng sử dụng Pinocchio nhằm giảm đáng kể số đơn vị tính toán cho mỗi giao dịch.

Bạn sẽ nhanh chóng nhận thấy rằng toàn bộ trạng thái đều được truy cập qua con trỏ.

Thay vì giải tuần tự hóa tài khoản token, dữ liệu từ AccountInfo được kiểm tra, sau đó một con trỏ được trả về.

Mỗi thuộc tính được truy cập qua một hàm và mọi giá trị không phải kiểu nguyên thủy đều trả về một tham chiếu để duy trì zero-copy. 

Để tìm hiểu thêm lý do cách này giảm đáng kể mức sử dụng CU, hãy đọc bài viết về tối ưu CU.

Pinocchio và Anchor

Anchor là một framework có quy ước rất phổ biến để phát triển chương trình Solana. Anchor được xem là ở mức trừu tượng cao hơn Pinocchio vì không chứa logic để cung cấp trực tiếp các cấu trúc nền như AccountInfo.

Thay vào đó, Anchor phụ thuộc vào crate solana-program đã đề cập và cung cấp các trait cùng macro để hợp lý hóa quy trình phát triển chương trình. Anchor cung cấp các mẫu instruction discriminator và logic giải tuần tự hóa tài khoản. Logic giải tuần tự hóa dựa vào Borsh, vốn yêu cầu sao chép dữ liệu sang một địa chỉ bộ nhớ khác vì không sử dụng zero-copy. 

Mặc dù sự tiện lợi của Anchor giúp đẩy nhanh quá trình phát triển chương trình Solana, đổi lại nó sử dụng nhiều CU hơn.

Ngược lại, Pinocchio là thư viện được thiết kế để thay thế solana-program khi nhà phát triển cần tinh chỉnh mức sử dụng tài nguyên tính toán. Thư viện này hoàn toàn không áp đặt quy ước và cho phép nhà phát triển tổ chức chương trình theo bất kỳ cách nào họ thấy phù hợp. Bố cục của mỗi dự án Pinocchio có thể hoàn toàn khác nhau, trong khi các dự án Anchor có cấu trúc được xác định rõ ràng. 

Thư viện Pinocchio không xử lý bất kỳ client binding hay phần triển khai nào. Ngược lại, Anchor hỗ trợ đầy đủ việc tạo IDL, có thể dùng ở phía client để tương tác với chương trình.

Nhà phát triển dùng Pinocchio phải tự viết hoặc sử dụng các công cụ khác như Shank và Codama. Chúng tôi sẽ trình bày những công cụ này trong phần Công cụ bổ trợ để xây dựng với Pinocchio bên dưới.

Pinocchio và Steel

Steel là một framework khác để viết chương trình Solana. Hiện được xây dựng trên solana-program, Steel cung cấp các macro, hàm và mẫu giúp việc viết chương trình an toàn, giàu tính biểu đạt trở nên dễ dàng.

Tính quy ước của Steel giúp mã dễ đọc mà vẫn duy trì tính mô-đun. Không giống Anchor, một framework phải dùng toàn bộ hoặc không dùng, nhà phát triển có thể chỉ chọn các thành phần Steel mà họ cần.

Macro account! của Steel dùng bytemuck để phân tích cấu trúc tài khoản, trong khi Pinocchio hoàn toàn không xử lý việc phân tích tài khoản. Steel cũng có các parser và assertion có thể nối chuỗi, giúp dễ dàng thêm các bước xác thực tùy chỉnh. Pinocchio không cung cấp sẵn các mẫu như vậy, nên nhà phát triển phải tự viết mẫu xác thực.

Tuy nhiên, đối với các lệnh gọi liên chương trình (CPI) phổ biến như System Program và Token Program, cả Pinocchio lẫn Steel đều cung cấp các mẫu giúp việc thực hiện những lệnh gọi này trở nên dễ dàng.

Pinocchio được tối ưu hóa cao nhưng để mọi chi tiết cho nhà phát triển quyết định. Steel là một lớp bọc mô-đun tiện dụng quanh thư viện solana-program, được thiết kế để cải thiện trải nghiệm nhà phát triển.

Cách tạo token bằng Pinocchio

Để minh họa một chương trình được viết bằng Pinocchio, chúng ta sẽ viết lại chương trình tạo token từ các ví dụ dành cho nhà phát triển Solana.

Đây là một chương trình đơn giản chỉ có một instruction để tạo token mint Token2022 và dùng phần mở rộng token Metadata nhằm lưu trữ thông tin về token. Metadata sẽ được cung cấp qua dữ liệu instruction chứa tên, ký hiệu và uri.

1. Định nghĩa entrypoint

Hãy bắt đầu bằng cách định nghĩa entrypoint của chương trình.

Chúng ta dùng macro entrypoint đầy đủ vì muốn sử dụng allocator và cơ chế xử lý panic mặc định của Pinocchio.

Mã
entrypoint!(process_instruction);

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

2. Định nghĩa cấu trúc dữ liệu instruction

Tiếp theo, chúng ta định nghĩa cấu trúc dữ liệu instruction sao cho khớp với các chương trình ví dụ khác. Để tiết kiệm thời gian phát triển, chúng ta sẽ dùng Borsh để giải tuần tự hóa và dành các phương pháp giải tuần tự hóa tối ưu hơn cho một bài viết khác.

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

3. Phân tích tài khoản và dữ liệu instruction

Bây giờ, hãy viết logic bên trong instruction processor.

Trước tiên, chúng ta phải phân rã các tài khoản từ danh sách tài khoản và giải tuần tự hóa dữ liệu instruction vào CreateTokenArgs.

Mã
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. Tạo tài khoản mint Token2022

Sau khi phân tích tài khoản và dữ liệu instruction, chúng ta gọi instruction CreateAccount của System program.

Bên dưới, chúng ta dùng struct CreateAccount từ `pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invoke.

Không giống khi tạo một mint SPL Token thông thường, chúng ta phải xác định dung lượng bổ sung mà các phần mở rộng token đang sử dụng yêu cầu.

Kích thước của phần mở rộng Metadata Pointer là cố định, còn phần mở rộng Token Metadata phải được tính động dựa trên các đối số được cung cấp.

Mã
 /// [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()?;

Sau khi CreateAccount được gọi, SystemProgram đã đăng ký chương trình Token2022 làm chủ sở hữu của tài khoản mint.

5. Khởi tạo phần mở rộng, tài khoản và các giá trị metadata

Tiếp theo, chúng ta phải thiết lập dữ liệu tài khoản bằng cách khởi tạo phần mở rộng Metadata Pointer, khởi tạo tài khoản Mint với chương trình Token2022 và khởi tạo các giá trị metadata mà chương trình nhận được dưới dạng đối số. 

Các CPI sau đây đến từ một nhánh đang được phát triển tích cực của crate pinocchio_token. Vì vậy, cần lưu ý rằng đoạn mã này có thể sớm lỗi thời do chức năng Token2022 dự kiến sẽ được tách khỏi crate SPL Token.

Mã
// 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()?;

Vậy là xong! 

Giờ đây, chúng ta đã có một token mint với metadata tự chứa, sử dụng Token2022 và được viết bằng Pinocchio.

Đoạn mã này vẫn có thể được cải thiện để đạt mức tối ưu tối đa, nhưng chúng tôi hy vọng nó giúp bạn hiểu cách viết chương trình bằng Pinocchio.

Công cụ bổ trợ để xây dựng với Pinocchio

Các công cụ dành riêng cho Pinocchio hiện còn ít nhưng đang ngày càng phong phú.

Bytemuck để tuần tự hóa và giải tuần tự hóa tài khoản

Nhà phát triển chương trình Pinocchio phải tự triển khai việc tuần tự hóa và giải tuần tự hóa tài khoản. Đây là quy trình tẻ nhạt và dễ xảy ra lỗi nếu làm thủ công. Bytemuck là một thư viện tuyệt vời giúp dễ dàng đọc và ghi mảng byte dưới dạng struct. Nhờ giới hạn lượng dữ liệu cần sao chép vào bộ nhớ, thư viện này được tối ưu khá tốt.

Borsh là một giải pháp khác khi làm việc với các tài khoản không có kích thước cố định. Tuy nhiên, nó sử dụng nhiều tài nguyên tính toán hơn và đây là một trong những lý do mọi người chọn Pinocchio thay vì Anchor.

Shank để tạo IDL

Vì Pinocchio là một thư viện nên nó không tích hợp sẵn chức năng tạo IDL như Anchor. IDL (Interface Definition Language) là một tệp JSON định nghĩa giao diện công khai của chương trình Solana, bao gồm các instruction, cấu trúc tài khoản và mã lỗi, qua đó cho phép tương tác theo tiêu chuẩn và đơn giản hóa việc phát triển phía client.

Để tạo IDL, chúng tôi khuyên dùng Shank. Crate này giúp nhà phát triển chú thích mã và dùng CLI để tạo IDL hợp lệ một cách cực kỳ dễ dàng. Việc thêm macro ShankAccount vào câu lệnh derive của một struct cho biết đó là một tài khoản có thể được tuần tự hóa và giải tuần tự hóa. Sau khi chạy shank CLI, cấu trúc này sẽ xuất hiện dưới dạng tài khoản có kiểu trong IDL để dùng cho việc tạo client.

Một macro quan trọng khác là ShankInstruction dành cho enum instruction của chương trình. Macro này cho phép dùng thuộc tính #[account] để chỉ định chỉ mục và quyền của từng tài khoản trong danh sách cho instruction cụ thể đó.

Xem kho mã nguồn shank-macro để tìm hiểu thêm về các chú thích mã hữu ích giúp dễ dàng tạo IDL cho những chương trình không dùng Anchor.

Codama để tạo client

Sau khi có IDL, bạn có thể dễ dàng tạo client bằng Codama. Nếu mã được tạo không phù hợp với nhu cầu, bạn sẽ phải viết client theo cách thủ công.

Tại Exo Tech, chúng tôi đã xây dựng một mẫu dự án Pinocchio để nhanh chóng khởi tạo các kho mã nguồn chương trình Solana. Hãy dùng thử và mở pull request nếu có bất kỳ cải tiến nào!

Tương lai của Pinocchio

Mặc dù được thiết kế để thay thế trực tiếp solana-program, Pinocchio vẫn chưa có đầy đủ tính năng tương đương. Một số sysvar chưa được hỗ trợ và các crate ngoài phần lõi chưa được hỗ trợ đầy đủ hoặc chưa tồn tại. Ví dụ, crate chương trình Pinocchio Token chưa hỗ trợ nhiều bên ký. Token2022 cũng chưa được hỗ trợ, dù đang trong quá trình phát triển.

Một trong những hạn chế đáng kể hơn khi dùng Pinocchio là tất cả SDK được phát triển cho các chương trình Solana khác đều sử dụng crate solana-program. Điều này có nghĩa là mỗi SDK đều yêu cầu quyền sở hữu AccountInfo hoặc dữ liệu được truyền qua lại, khiến việc tương tác với một chương trình phát triển bằng Pinocchio trở nên cực kỳ khó khăn. 

Khi tích hợp với chương trình của bên thứ ba, việc phải viết logic CPI tùy chỉnh cho từng instruction là rất phổ biến. Vấn đề này cuối cùng có thể được giải quyết bằng các trình tạo mã như Codama, nhưng hiện vẫn chưa đạt đến mức đó.

Cần lưu ý rằng Pinocchio vẫn đang được phát triển tích cực và chưa được kiểm định. Cộng đồng vẫn đang bổ sung các sysvar còn lại vào SDK, đồng thời cải thiện khả năng hỗ trợ các chương trình SPL quan trọng như Token và Token2022.

Cách đóng góp cho Pinocchio

Pinocchio còn rất nhiều hạng mục dễ tiếp cận để đóng góp.

Có các issue đang mở và pull request hiện có cần thêm sự hỗ trợ. Hãy tham gia thảo luận hoặc đơn giản là mở một pull request để các maintainer xem xét!

Kết luận

So với các giải pháp trước đây, Pinocchio là thư viện có hiệu năng vượt trội để viết chương trình Solana. Việc cung cấp cho nhà phát triển nhiều khả năng tùy chỉnh entrypoint của chương trình hơn và dùng zero-copy để truy cập đầu vào chương trình có thể giúp giảm mức sử dụng CU. Tuy nhiên, đây vẫn là một thư viện mới và chưa đầy đủ tính năng. Tại thời điểm viết bài, thư viện chưa được kiểm định, vì vậy hãy thận trọng khi sử dụng.

Khi cân nhắc có nên dùng Pinocchio hay không, điều quan trọng là phải đánh giá những điểm đánh đổi so với các thư viện và framework khác.

Các framework có quy ước như Anchor sẽ đẩy nhanh quá trình phát triển chương trình và dễ bảo trì hơn, khiến chúng trở thành lựa chọn tuyệt vời khi tốc độ đưa sản phẩm ra thị trường là yếu tố quan trọng.

Khi sản phẩm đã ổn định và xử lý khối lượng giao dịch lớn, việc tối ưu chương trình Solana bằng một thư viện như Pinocchio có thể phù hợp hơn.

Tài nguyên bổ sung

Để tìm hiểu thêm, hãy xem phần trình bày của Febo tại Solana Accelerate 2025 và khám phá các tài nguyên học tập sau:

Đăng ký nhận tin từ Helius

Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài

Hình ảnh phóng to