Cross Program Invocation (CPI)

يشير Cross Program Invocation (CPI) إلى عندما يقوم برنامج باستدعاء تعليمات برنامج آخر. هذا يسمح بقابلية التركيب لبرامج سولانا.

يمكنك التفكير في التعليمات كنقاط نهاية API يكشفها البرنامج للشبكة و CPI كواجهة API تستدعي داخليًا واجهة API أخرى.

Cross Program InvocationCross Program Invocation

النقاط الرئيسية

  • Cross Program Invocation تمكّن تعليمات برنامج سولانا من استدعاء تعليمات برنامج آخر مباشرة.
  • امتيازات التوقيع من البرنامج المستدعي تمتد إلى البرنامج المستدعى.
  • عند إجراء Cross Program Invocation، يمكن للبرامج التوقيع نيابة عن PDAs المشتقة من معرف البرنامج الخاص بها.
  • يمكن للبرنامج المستدعى إجراء المزيد من CPIs إلى برامج أخرى، حتى عمق 4.

ما هو CPI؟

Cross Program Invocation (CPI) هو عندما يقوم برنامج باستدعاء تعليمات برنامج آخر.

كتابة تعليمات البرنامج باستخدام CPI تتبع نفس النمط مثل بناء تعليمات لإضافتها إلى معاملة. في الأساس، يجب أن تحدد كل تعليمات CPI:

  • عنوان البرنامج: يحدد البرنامج المراد استدعاؤه
  • الحسابات: يسرد كل حساب تقرأ منه التعليمات أو تكتب إليه، بما في ذلك البرامج الأخرى
  • بيانات التعليمات: تحدد أي تعليمات يتم استدعاؤها على البرنامج، بالإضافة إلى أي بيانات تحتاجها التعليمات (وسيطات الدالة)

عندما يقوم برنامج بإجراء Cross Program Invocation (CPI) إلى برنامج آخر:

  • امتيازات التوقيع من المعاملة الأولية تمتد إلى البرنامج المستدعى (مثال: A->B)
  • يمكن للبرنامج المستدعى إجراء المزيد من Cross Program Invocation إلى برامج أخرى، حتى عمق 4 (مثال: B->C, C->D)
  • يمكن للبرامج "التوقيع" نيابة عن PDAs المشتقة من معرف البرنامج الخاص بها

تحدد بيئة تشغيل برنامج سولانا ثابتًا max_instruction_stack_depth يسمى MAX_INSTRUCTION_STACK_DEPTH بقيمة 5. هذا يمثل الحد الأقصى لارتفاع مكدس استدعاء تعليمات البرنامج. يبدأ ارتفاع المكدس عند 1 للمعاملة الأولية ويزداد بمقدار 1 في كل مرة يستدعي فيها برنامج تعليمة أخرى. هذا الإعداد يحد من عمق الاستدعاء لـ CPIs إلى 4.

عندما تتم معالجة معاملة، تمتد امتيازات الحساب من برنامج إلى آخر. إليك ما يعنيه ذلك:

لنفترض أن البرنامج A يتلقى تعليمة تحتوي على:

  • حساب وقّع على المعاملة
  • حساب يمكن الكتابة فيه (قابل للتعديل)

عندما يقوم البرنامج A بإجراء Cross Program Invocation إلى البرنامج B:

  • يحصل البرنامج B على استخدام هذه الحسابات نفسها بأذوناتها الأصلية
  • يمكن للبرنامج B التوقيع باستخدام حساب الموقّع
  • يمكن للبرنامج B الكتابة في الحساب القابل للكتابة
  • يمكن للبرنامج B حتى تمرير هذه الأذونات نفسها إلى الأمام إذا قام بإجراء Cross Program Invocation الخاصة به

Cross Program Invocation

تتعامل الدالة invoke مع Cross Program Invocation التي لا تتطلب موقعين PDA. تستدعي الدالة الوظيفة invoke_signed مع مصفوفة signers_seeds فارغة، مما يشير إلى عدم الحاجة إلى PDAs للتوقيع.

Invoke Function
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}

توضح الأمثلة التالية كيفية إجراء Cross Program Invocation باستخدام إطار عمل Anchor و Native Rust. تتضمن البرامج المثال تعليمة واحدة تنقل SOL من حساب إلى آخر باستخدام Cross Program Invocation.

إطار عمل Anchor

تقدم الأمثلة التالية ثلاث طرق لإنشاء استدعاءات عبر البرامج (Cross Program Invocation) في برنامج Anchor، كل منها بمستوى مختلف من التجريد. جميع الأمثلة تعمل بنفس الطريقة. الغرض الرئيسي هو إظهار تفاصيل تنفيذ استدعاء CPI.

  • المثال 1: يستخدم CpiContext من Anchor ودالة مساعدة لبناء تعليمات CPI.
  • المثال 2: يستخدم دالة system_instruction::transfer من حزمة solana_program لبناء تعليمات CPI. المثال 1 يجرد هذا التنفيذ.
  • المثال 3: يبني تعليمات CPI يدوياً. هذا الأسلوب مفيد عندما لا توجد حزمة للمساعدة في بناء التعليمات.
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");
#[program]
pub mod cpi {
use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.sender.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();
let cpi_context = CpiContext::new(
program_id,
Transfer {
from: from_pubkey,
to: to_pubkey,
},
);
transfer(cpi_context, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(mut)]
sender: Signer<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

Rust الأصلي

يوضح المثال التالي كيفية إجراء CPI من برنامج مكتوب بلغة Rust الأصلية. يتضمن البرنامج تعليمة واحدة تقوم بتحويل SOL من حساب إلى آخر باستخدام CPI. يستخدم ملف الاختبار LiteSVM لاختبار البرنامج.

use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
// Declare program entrypoint
entrypoint!(process_instruction);
// Define program instructions
#[derive(BorshDeserialize)]
enum ProgramInstruction {
SolTransfer { amount: u64 },
}
impl ProgramInstruction {
fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)
}
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Deserialize instruction data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [sender_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Verify the sender is a signer
if !sender_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create and invoke the transfer instruction
let transfer_ix = system_instruction::transfer(
sender_info.key,
recipient_info.key,
amount,
);
invoke(
&transfer_ix,
&[
sender_info.clone(),
recipient_info.clone(),
system_program_info.clone(),
],
)?;
Ok(())
}
}
}

استدعاءات عبر البرامج مع موقعين PDA

تتعامل دالة invoke_signed مع استدعاءات CPI التي تتطلب موقعين PDA. تأخذ الدالة البذور (seeds) لاشتقاق موقعي PDA كـ signer_seeds.

يمكنك الرجوع إلى صفحة عناوين مشتقة من البرنامج للحصول على تفاصيل حول كيفية اشتقاق PDAs.

Invoke Signed
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult {
// --snip--
invoke_signed_unchecked(instruction, account_infos, signers_seeds)
}

عند معالجة تعليمة تتضمن CPI، يقوم وقت تشغيل سولانا داخلياً باستدعاء create_program_address باستخدام signers_seeds و program_id للبرنامج المستدعي. عندما يتم التحقق من صحة PDA، يتم إضافة العنوان كموقع صالح.

توضح الأمثلة التالية كيفية إجراء CPI مع موقعين PDA باستخدام إطار عمل Anchor و Rust الأصلي. تتضمن برامج المثال تعليمة واحدة تقوم بتحويل SOL من PDA إلى حساب المستلم باستخدام CPI موقع بواسطة PDA.

إطار عمل Anchor

تتضمن الأمثلة التالية ثلاثة أساليب لتنفيذ استدعاءات البرامج المتقاطعة (CPIs) في برنامج Anchor، كل منها بمستوى مختلف من التجريد. جميع الأمثلة متكافئة وظيفيًا. الغرض الرئيسي هو توضيح تفاصيل تنفيذ الـ CPI.

  • المثال 1: يستخدم CpiContext من Anchor ودالة مساعدة لبناء تعليمات الـ CPI.
  • المثال 2: يستخدم دالة system_instruction::transfer من حزمة solana_program لبناء تعليمات الـ CPI. المثال 1 هو تجريد لهذا التنفيذ.
  • المثال 3: يبني تعليمات الـ CPI يدويًا. هذا النهج مفيد عندما لا تتوفر حزمة للمساعدة في بناء التعليمات التي تريد استدعاءها.
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
declare_id!("BrcdB9sV7z9DvF9rDHG263HUxXgJM3iCQdF36TcxbFEn");
#[program]
pub mod cpi {
use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.pda_account.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key();
let bump_seed = ctx.bumps.pda_account;
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let cpi_context = CpiContext::new(
program_id,
Transfer {
from: from_pubkey,
to: to_pubkey,
},
)
.with_signer(signer_seeds);
transfer(cpi_context, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(
mut,
seeds = [b"pda", recipient.key().as_ref()],
bump,
)]
pda_account: SystemAccount<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

لغة Rust الأصلية

يوضح المثال التالي كيفية إجراء CPI مع موقعين PDA من برنامج مكتوب بلغة Rust الأصلية. يتضمن البرنامج تعليمة واحدة تقوم بتحويل عملة SOL من حساب PDA إلى حساب المستلم باستخدام CPI موقع بواسطة الـ PDA. يستخدم ملف الاختبار LiteSVM لاختبار البرنامج.

use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
// Declare program entrypoint
entrypoint!(process_instruction);
// Define program instructions
#[derive(BorshDeserialize)]
enum ProgramInstruction {
SolTransfer { amount: u64 },
}
impl ProgramInstruction {
fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)
}
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Deserialize instruction data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [pda_account_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Derive PDA and verify it matches the account provided by client
let recipient_pubkey = recipient_info.key;
let seeds = &[b"pda", recipient_pubkey.as_ref()];
let (expected_pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account_info.key {
return Err(ProgramError::InvalidArgument);
}
// Create the transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account_info.key,
recipient_info.key,
amount,
);
// Create signer seeds for PDA
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];
// Invoke the transfer instruction with PDA as signer
invoke_signed(
&transfer_ix,
&[
pda_account_info.clone(),
recipient_info.clone(),
system_program_info.clone(),
],
signer_seeds,
)?;
Ok(())
}
}
}

Is this page helpful?

جدول المحتويات

تعديل الصفحة