Cross Program Invocation (CPI)

Cross Program Invocation(CPI)는 하나의 프로그램이 다른 프로그램의 명령어를 호출하는 것을 의미합니다. 이를 통해 Solana 프로그램의 구성 가능성이 실현됩니다.

명령어를 프로그램이 네트워크에 노출하는 API 엔드포인트로, CPI를 한 API가 내부적으로 다른 API를 호출하는 것으로 생각할 수 있습니다.

Cross Program InvocationCross Program Invocation

주요 포인트

  • Cross Program Invocations는 Solana 프로그램 명령어가 다른 프로그램의 명령어를 직접 호출할 수 있게 합니다.
  • 호출자 프로그램의 서명자 권한이 피호출자 프로그램으로 확장됩니다.
  • Cross Program Invocation을 수행할 때, 프로그램은 자체 프로그램 ID에서 파생된 PDA를 대신하여 서명할 수 있습니다.
  • 피호출자 프로그램은 다른 프로그램에 대해 추가 CPI를 수행할 수 있으며, 최대 깊이는 4입니다.

CPI란 무엇인가요?

Cross Program Invocation(CPI)는 하나의 프로그램이 다른 프로그램의 명령어를 호출하는 것입니다.

CPI가 포함된 프로그램 명령어를 작성하는 것은 instruction을 트랜잭션에 추가하기 위해 구성하는 것과 동일한 패턴을 따릅니다. 내부적으로 각 CPI 명령어는 다음을 지정해야 합니다:

  • 프로그램 주소: 호출할 프로그램을 지정합니다
  • 계정: 명령어가 읽거나 쓰는 모든 계정을 나열합니다(다른 프로그램 포함)
  • instruction data: 프로그램에서 호출할 명령어와 명령어에 필요한 데이터(함수 인수)를 지정합니다

프로그램이 다른 프로그램에 Cross Program Invocation(CPI)을 수행할 때:

  • 초기 트랜잭션의 서명자 권한은 호출된 프로그램으로 확장됩니다(예: A->B)
  • 호출된 프로그램은 다른 프로그램에 대해 최대 4단계 깊이까지 추가 CPI를 수행할 수 있습니다(예: B->C, C->D)
  • 프로그램은 자신의 프로그램 ID에서 파생된 PDA를 대신하여 "서명"할 수 있습니다

Solana 프로그램 런타임은 max_instruction_stack_depth 상수 MAX_INSTRUCTION_STACK_DEPTH 를 5로 설정합니다. 이는 프로그램 명령 호출 스택의 최대 높이를 나타냅니다. 스택 높이는 초기 트랜잭션에서 1로 시작하고 프로그램이 다른 명령을 호출할 때마다 1씩 증가합니다. 이 설정은 CPI의 호출 깊이를 4로 제한합니다.

트랜잭션이 처리될 때, 계정 권한은 한 프로그램에서 다른 프로그램으로 확장됩니다. 이것이 의미하는 바는 다음과 같습니다:

프로그램 A가 다음을 포함한 명령을 받는다고 가정해 봅시다:

  • 트랜잭션에 서명한 계정
  • 쓰기가 가능한 계정(변경 가능)

프로그램 A가 프로그램 B에 CPI를 수행할 때:

  • 프로그램 B는 이러한 동일한 계정을 원래 권한으로 사용할 수 있습니다
  • 프로그램 B는 서명자 계정으로 서명할 수 있습니다
  • 프로그램 B는 쓰기 가능한 계정에 쓸 수 있습니다
  • 프로그램 B는 자체 CPI를 수행하는 경우 이러한 동일한 권한을 전달할 수도 있습니다

Cross Program Invocation

invoke 함수는 PDA 서명자가 필요하지 않은 CPI를 처리합니다. 이 함수는 빈 signers_seeds 배열과 함께 invoke_signed 함수를 호출하여 서명에 필요한 PDA가 없음을 나타냅니다.

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

다음 예제는 Anchor Framework와 네이티브 Rust를 사용하여 CPI를 수행하는 방법을 보여줍니다. 예제 프로그램에는 CPI를 사용하여 한 계정에서 다른 계정으로 SOL을 전송하는 단일 명령이 포함되어 있습니다.

Anchor 프레임워크

다음 예제는 Anchor 프로그램에서 Cross Program Invocations(CPI)를 생성하는 세 가지 방법을 보여주며, 각각 다른 수준의 추상화를 사용합니다. 모든 예제는 동일한 방식으로 작동합니다. 주요 목적은 CPI의 구현 세부 사항을 보여주는 것입니다.

  • 예제 1: Anchor의 CpiContext 및 헬퍼 함수를 사용하여 CPI 명령어를 구성합니다.
  • 예제 2: solana_program 크레이트의 system_instruction::transfer 함수를 사용하여 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

다음 예제는 네이티브 Rust로 작성된 프로그램에서 CPI를 수행하는 방법을 보여줍니다. 이 프로그램은 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 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 서명자를 사용한 Cross Program Invocations

invoke_signed 함수는 PDA 서명자가 필요한 CPI를 처리합니다. 이 함수는 서명자 PDA를 파생하기 위한 seed를 signer_seeds로 받습니다.

PDA 파생 방법에 대한 자세한 내용은 Program Derived Address 페이지를 참조하세요.

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를 포함하는 명령어를 처리할 때, Solana 런타임은 내부적으로 호출 프로그램의 signers_seedsprogram_id를 사용하여 create_program_address를 호출합니다. 유효한 PDA가 확인되면, 해당 주소는 유효한 서명자로 추가됩니다.

다음 예제는 Anchor 프레임워크와 네이티브 Rust를 사용하여 PDA 서명자로 CPI를 수행하는 방법을 보여줍니다. 예제 프로그램은 PDA가 서명한 CPI를 사용하여 PDA에서 수신자 계정으로 SOL을 전송하는 단일 명령어를 포함합니다.

Anchor 프레임워크

다음 예제들은 Anchor 프로그램에서 Cross Program Invocations(CPIs)를 구현하는 세 가지 접근 방식을 포함하며, 각각 다른 수준의 추상화를 보여줍니다. 모든 예제는 기능적으로 동일합니다. 주요 목적은 CPI의 구현 세부 사항을 설명하는 것입니다.

  • 예제 1: Anchor의 CpiContext 및 헬퍼 함수를 사용하여 CPI 명령을 구성합니다.
  • 예제 2: solana_program 크레이트에서 system_instruction::transfer 함수를 사용하여 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

다음 예제는 네이티브 Rust로 작성된 프로그램에서 PDA 서명자를 사용하여 CPI를 수행하는 방법을 보여줍니다. 이 프로그램은 PDA가 서명한 CPI를 사용하여 PDA에서 수신자 계정으로 SOL을 전송하는 단일 명령을 포함합니다. 테스트 파일은 프로그램을 테스트하기 위해 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?

목차

페이지 편집