[Solana] PDA, CPI , Program 2

드림보이즈·2025년 4월 2일

2025 솔라나 해커톤

목록 보기
4/7

존나 어렵다 대가리 터질 거 같다

목표 : PDA, CPI를 이해하고, Program에 활용해본다.

주요 키워드 : PDA,CPI, Program


1. PDA = Program Derived Address

이더리움이든 솔라나든 키 페어를 생성할 때,
특정 타원 곡선을 이용한다. 이더리움은 secpk-256이라는 특정 곡선 상에 있는 점을
공개키로 사용한다.

마찬가지로 솔라나는 Ed25519 라는 곡선을 사용한다.

모든 개인키는 이 곡선 상의 점이다.
솔라나의 모든 데이터는 Account에 저장된다고 했다.
만약 내가 게임을 만드는데, 데이터를 저장하기 위해 Account들이 많이 필요해질 것이다.
이 Account 들을 검색하려면? 하나하나 너무 다른 주소를 직접 관리해야 한다.

검색하기 쉽게 만들 수는 없을까?

만약 인자를 2개 넣어서 주소가 나온다면 일일히 저장할 필요없을텐데 말이다.

Seed와 Bump, ProgramID를 사용해서 주소를 생성할 수 있다.
Seed는 내 맘대로 "1","2"...
Bump는 숫자로, 유효한 주소 형식이 나올 때 까지 변하는 숫자다.

ProgramID는 이 주소를 제어할 수 있는 프로그램의 ID다.
그냥 일반 사용자를 위한 주소를 만드는게 목적이 아니다.

프로그램을 위해서, 프로그램이 데이터를 쉽게 찾기 위해 만든 것이다.

2. CPI = Cross Program Invocations

이더리움의 call과 비슷한 기능이다.
프로그램이 다른 프로그램을 호출한다.
뜬금없이 전혀 다른 사람이 만든 함수를 호출할 수도 있을 것이다.
가능은 하지만, 이런 경우는 적고, 대부분
한 팀에서 여러 프로그램을 만들어서 호출을 하게끔 작성할 것이다.
호출당하는, callee 프로그램에선, 아무나 나를 호출하지 못하게 권한 설정을 줘야 할 것이 아닌가?
그 때 PDA를 활용한다. 마치 비밀번호처럼.

PDA with CPI

PDA는 특정 인자들로 주소를 만드는 방법이지만, 블록체인에 계정을 만들어서 데이터를 저장하기 위한 것으로 쓰이는 경우가 드물다.

비밀번호

역할을 한다. 아까 위에서 시드, 프로그램 아이디!!!, 범프를 이용해서 주소를 만든대매.
그럼 Caller가 Callee를 호출할 때 좋든 싫든 프로그램 아이디를 전달할 것이고,
서로 동일한 시드만 가지고 있으면 나를 호출한게 맞는지 알아낼 수 있는 것 아니겠는가?

Caller => Callee

PDA만 전달한다.
Callee는 호출한 사람의 ID를 내장된 기능으로 알 수 있다. 따로 안 줘도 된다.
하드코딩된 시드와 프로그램 아이디로 PDA를 만들어서 비교한다.
같으면 호출자가 권한자인 걸 알 수 있는 것이다.

invoke(instructions, account_infos) : PDA 안 쓰는 경우

솔라나가 제공하는 함수다.

  • Instructions : 호출할 프로그램에서 뭘 실행할 건지
  • account_infos : 호출할 프로그램에서 사용될 계정 정보
    (Program ID는 알아서 전달됨)

invoke_signed(instructions, account_infos, signer_seeds)


4. 예제 프로그램 작성

목표 :

  • 프로그램 A에서 CPI로 프로그램B를 호출
  • 프로그램 B는 PDA로 검증 후 A가 맞으면 Ok출력

Program A (Caller)

use anchor_lang::prelude::*;
use program_b::program::ProgramB;
declare_id!("9QUV2hw8McjHndxM8Pu6pvwc6o6owgSRgsVGcQZt6gkC");

#[program]
pub mod program_derived_address {
    use anchor_lang::solana_program::{ program::{ invoke_signed }, system_instruction };

    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Greetings from program A");

        let pda_address = ctx.accounts.pda_account.key();
        let signer_address = ctx.accounts.signer.key();
        let bump = ctx.bumps.pda_account;

        let instruction = &system_instruction::transfer(
            &pda_address,
            &signer_address,
            1_000_000_000
        );

        let account_infos = [
            ctx.accounts.pda_account.to_account_info(),
            ctx.accounts.signer.to_account_info(),
            ctx.accounts.system_program.to_account_info(),
        ];

        let signer_seeds: &[&[&[u8]]] = &[&[b"ackee", signer_address.as_ref(), &[bump]]];

        invoke_signed(instruction, &account_infos, signer_seeds)?;
        // invoke(instruction, &account_infos)?;

        let cpi_context = CpiContext::new_with_signer(
            ctx.accounts.program_b.to_account_info(),
            program_b::cpi::accounts::Initialize {
                pda_account: ctx.accounts.pda_account.to_account_info(),
            },
            signer_seeds
        );

        program_b::cpi::initialize(cpi_context)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// CHECK: This account is a PDA derived using the seeds [b"ackee", signer.key().as_ref()] and is safe to use.

    #[account(
        mut,
        seeds = [b"ackee",signer.key().as_ref()],
        bump,
    )]
    pub pda_account: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub program_b: Program<'info, ProgramB>,
}

Program B (callee)

use anchor_lang::prelude::*;

declare_id!("6Cet5HYjcckrpZy2az5qqFgLSC1ibstY93FWfN4XT4GY");

#[program]
pub mod program_b {
    use super::*;

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        msg!("Greetings from program B");
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub pda_account: Signer<'info>,
}

Tests.ts

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ProgramDerivedAddress } from "../target/types/program_derived_address";
import { ProgramB } from "../target/types/program_b";


describe("program-derived-address", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.programDerivedAddress as Program<ProgramDerivedAddress>;
  const program_b = anchor.workspace.ProgramB as Program<ProgramDerivedAddress>;

  let signer = anchor.web3.Keypair.generate();



  it("Is initialized!", async () => {



    let [pda_address,bump] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("ackee"), signer.publicKey.toBuffer()],
      program.programId
    );
    await airdrop(program.provider.connection,pda_address, 500_000_000_000);



    // Add your test here.

      const tx = await program.methods.initialize().accounts({
        pdaAccount : pda_address,
        signer : signer.publicKey,
        systemProgram : anchor.web3.SystemProgram.programId,
        programB: program_b.programId
      }).signers([signer]).rpc();






    console.log("Your transaction signature", tx);
  });
});


export async function airdrop(
  connection : any,
  address : any,
  amount = 5_000_000_000
) {
  await connection.confirmTransaction(
    await connection.requestAirdrop(address, amount),
    'confirmed'
  );
}
profile
시리즈 클릭하셔서 카테고리 별로 편하게 보세용

0개의 댓글