Cross Program Invocation
크로스 프로그램 호출(CPI)은 하나의 솔라나 프로그램이 다른 프로그램의 명령어를 직접 호출할 때 발생합니다. 이를 통해 프로그램 간 구성이 가능해집니다. 솔라나 명령어를 네트워크에 노출되는 API 엔드포인트로 생각한다면, CPI는 하나의 엔드포인트가 내부적으로 다른 엔드포인트를 호출하는 것과 같습니다.
CPI를 수행할 때, 프로그램은 자신의 프로그램 ID에서 파생된 PDA를 대신하여 서명할 수 있습니다. 이러한 서명 권한은 호출자 프로그램에서 피호출자 프로그램으로 확장됩니다.
크로스 프로그램 호출 예시
CPI를 수행할 때, 계정 권한은 한 프로그램에서 다른 프로그램으로 확장됩니다. 프로그램 A가 서명자 계정과 쓰기 가능한 계정이 포함된 명령어를 받았다고 가정해 봅시다. 프로그램 A가 프로그램 B에 CPI를 수행하면, 프로그램 B는 프로그램 A와 동일한 계정을 원래 권한 그대로 사용할 수 있습니다. (즉, 프로그램 B는 서명자 계정으로 서명할 수 있고 쓰기 가능한 계정에 쓸 수 있습니다.) 프로그램 B가 자체적으로 CPI를 수행하면, 최대 깊이 4까지 동일한 권한을 전달할 수 있습니다.
프로그램 명령어 호출의 최대 높이는 max_instruction_stack_depth라고 하며, MAX_INSTRUCTION_STACK_DEPTH 상수인 5로 설정되어 있습니다.
스택 높이는 초기 트랜잭션에서 1로 시작하고 프로그램이 다른 명령어를 호출할 때마다 1씩 증가하여, CPI의 호출 깊이를 4로 제한합니다.
PDA 서명자가 있는 CPI
CPI에 PDA 서명자가 필요한 경우,
invoke_signed
함수가 사용됩니다. 이 함수는 서명자 PDA를 도출하는 데
사용되는 서명자 시드를 받습니다. Solana 런타임은
내부적으로 호출 프로그램의 signers_seeds와 program_id를 사용하여
create_program_address를
호출합니다. PDA가 확인되면, 해당 주소는
유효한 서명자로 추가됩니다.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
아래 예제들은 Anchor와 네이티브 Rust를 사용하여 PDA 서명자로 CPI를 만드는 방법을 보여줍니다. 각 예제는 PDA가 서명한 CPI를 사용하여 PDA에서 수신자 계정으로 SOL을 전송하는 단일 명령을 포함합니다.
Anchor
다음 예제들은 Anchor 프로그램에서 CPI를 구현하는 세 가지 접근 방식을 보여줍니다. 이 예제들은 기능적으로 동일하지만, 각각 다른 수준의 추상화를 보여줍니다.
- 예제 1: Anchor의
CpiContext및 헬퍼 함수를 사용합니다. - 예제 2:
solana_program크레이트의system_instruction::transfer함수를 사용합니다. (예제 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
아래 예제는 네이티브 Rust로 작성된 프로그램에서 PDA 서명자를 사용하여 CPI를 만드는 방법을 보여줍니다. 이 프로그램은 PDA 계정에서 다른 계정으로 SOL을 전송하는 단일 명령을 포함합니다. 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 entrypointentrypoint!(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 datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [pda_account_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Derive PDA and verify it matches the account provided by clientlet 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 instructionlet transfer_ix = system_instruction::transfer(pda_account_info.key,recipient_info.key,amount,);// Create signer seeds for PDAlet signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];// Invoke the transfer instruction with PDA as signerinvoke_signed(&transfer_ix,&[pda_account_info.clone(),recipient_info.clone(),system_program_info.clone(),],signer_seeds,)?;Ok(())}}}
PDA 서명자가 없는 CPI
CPI에 PDA 서명자가 필요하지 않은 경우,
invoke
함수가 사용됩니다. invoke 함수는 빈 signers_seeds 배열과 함께
invoke_signed 함수를 호출합니다. 빈 서명자 배열은 서명에 필요한 PDA가 없음을
나타냅니다.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
아래 예제는 Anchor와 네이티브 Rust를 사용하여 CPI를 수행합니다. 한 계정에서 다른 계정으로 SOL을 전송하는 단일 명령어가 포함되어 있습니다.
Anchor
다음 예제들은 Anchor 프로그램에서 CPI를 구현하는 세 가지 접근 방식을 보여줍니다. 이 예제들은 기능적으로 동일하지만, 각각 다른 수준의 추상화를 보여줍니다.
- 예제 1: Anchor의
CpiContext및 헬퍼 함수를 사용합니다. - 예제 2:
solana_program크레이트의system_instruction::transfer함수를 사용합니다. - 예제 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
다음 예제는 네이티브 Rust로 작성된 프로그램에서 CPI를 수행하는 방법을 보여줍니다. 한 계정에서 다른 계정으로 SOL을 전송하는 단일 명령어가 포함되어 있습니다. (테스트 파일은 프로그램을 테스트하기 위해 LiteSVM을 사용합니다.)
use borsh::BorshDeserialize;use solana_program::{account_info::AccountInfo,entrypoint,entrypoint::ProgramResult,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,};// Declare program entrypointentrypoint!(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 datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [sender_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Verify the sender is a signerif !sender_info.is_signer {return Err(ProgramError::MissingRequiredSignature);}// Create and invoke the transfer instructionlet 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(())}}}
Is this page helpful?