Solana 문서프로그램 개발하기Rust 프로그램

Rust 프로그램 구조

Rust로 작성된 Solana 프로그램은 코드 구성 방식에 유연성을 제공하는 최소한의 구조적 요구사항만 있습니다. 유일한 요구사항은 프로그램에 entrypoint가 있어야 한다는 것으로, 이는 프로그램 실행이 시작되는 지점을 정의합니다.

프로그램 구조

파일 구조에 대한 엄격한 규칙은 없지만, Solana 프로그램은 일반적으로 다음과 같은 공통 패턴을 따릅니다:

  • entrypoint.rs: 들어오는 명령어를 라우팅하는 엔트리포인트를 정의합니다.
  • state.rs: 프로그램별 상태(계정 데이터)를 정의합니다.
  • instructions.rs: 프로그램이 실행할 수 있는 명령어를 정의합니다.
  • processor.rs: 각 명령어에 대한 비즈니스 로직을 구현하는 명령어 핸들러(함수)를 정의합니다.
  • error.rs: 프로그램이 반환할 수 있는 사용자 정의 오류를 정의합니다.

Solana 프로그램 라이브러리에서 예제를 찾을 수 있습니다.

예제 프로그램

여러 명령어가 있는 네이티브 Rust 프로그램을 구축하는 방법을 보여주기 위해, 두 가지 명령어를 구현하는 간단한 카운터 프로그램을 살펴보겠습니다:

  1. InitializeCounter: 초기값으로 새 계정을 생성하고 초기화합니다.
  2. IncrementCounter: 기존 계정에 저장된 값을 증가시킵니다.

간단하게 하기 위해 프로그램은 단일 lib.rs 파일에 구현되지만, 실제로는 더 큰 프로그램을 여러 파일로 분할하는 것이 좋을 수 있습니다.

새 프로그램 만들기

먼저, 표준 cargo init 명령어와 --lib 플래그를 사용하여 새 Rust 프로젝트를 만듭니다.

Terminal
cargo init counter_program --lib

프로젝트 디렉토리로 이동합니다. 기본 src/lib.rsCargo.toml 파일이 보일 것입니다.

Terminal
cd counter_program

다음으로, solana-program 의존성을 추가하세요. 이것은 Solana 프로그램을 빌드하는 데 필요한 최소한의 의존성입니다.

Terminal
cargo add solana-program@1.18.26

다음으로, 아래 코드 조각을 Cargo.toml에 추가하세요. 이 설정을 포함하지 않으면 프로그램을 빌드할 때 target/deploy 디렉토리가 생성되지 않습니다.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Cargo.toml 파일은 다음과 같이 보여야 합니다:

Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18.26"

프로그램 진입점

Solana 프로그램 진입점은 프로그램이 호출될 때 실행되는 함수입니다. 진입점은 다음과 같은 기본 정의를 가지며, 개발자는 자유롭게 진입점 함수의 구현을 만들 수 있습니다.

간단하게 하기 위해, solana_program 크레이트의 entrypoint! 매크로를 사용하여 프로그램에서 진입점을 정의하세요.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

lib.rs의 기본 코드를 다음 코드로 대체하세요. 이 코드 조각은:

  1. solana_program에서 필요한 의존성을 가져옵니다
  2. entrypoint! 매크로를 사용하여 프로그램 진입점을 정의합니다
  3. 명령어를 적절한 핸들러 함수로 라우팅하는 process_instruction 함수를 구현합니다
lib.rs
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}

entrypoint! 매크로는 다음과 같은 타입 시그니처를 가진 함수를 인자로 필요로 합니다:

pub type ProcessInstruction =
fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;

Solana 프로그램이 호출되면, 진입점은 입력 데이터(바이트로 제공됨)를 역직렬화하여 세 가지 값으로 변환하고 이를 process_instruction 함수에 전달합니다:

  • program_id: 호출되는 프로그램(현재 프로그램)의 공개 키
  • accounts: 호출되는 명령어에 필요한 계정에 대한 AccountInfo
  • instruction_data: 실행할 명령어와 필요한 인자를 지정하는 프로그램에 전달되는 추가 데이터

이 세 가지 매개변수는 클라이언트가 프로그램을 호출하는 명령어를 만들 때 제공해야 하는 데이터와 직접적으로 대응됩니다.

프로그램 상태 정의하기

Solana 프로그램을 구축할 때, 일반적으로 프로그램의 상태를 정의하는 것부터 시작합니다 - 이는 프로그램이 생성하고 소유한 계정에 저장될 데이터입니다.

프로그램 상태는 프로그램 계정의 데이터 레이아웃을 나타내는 Rust 구조체를 사용하여 정의됩니다. 프로그램의 다양한 유형의 계정을 나타내기 위해 여러 구조체를 정의할 수 있습니다.

계정을 다룰 때, 프로그램의 데이터 타입을 계정의 데이터 필드에 저장된 원시 바이트로 변환하고 다시 변환하는 방법이 필요합니다:

  • 직렬화(Serialization): 데이터 타입을 바이트로 변환하여 계정의 데이터 필드에 저장
  • 역직렬화(Deserialization): 계정에 저장된 바이트를 다시 데이터 타입으로 변환

Solana 프로그램 개발에 어떤 직렬화 형식도 사용할 수 있지만, Borsh가 일반적으로 사용됩니다. Solana 프로그램에서 Borsh를 사용하려면:

  1. "borsh" 크레이트를 "Cargo.toml"의 의존성으로 추가하세요:
[dependencies]
borsh = "0.10.3"
borsh-derive = "0.10.3"
  1. Borsh 트레이트를 가져오고 derive 매크로를 사용하여 구조체에 트레이트를 구현하세요:
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
pub count: u32,
}

"Counter" 구조체를 "lib.rs"에 추가하여 프로그램 상태를 정의하세요. 이 구조체는 초기화 및 증가 명령 모두에서 사용됩니다.

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
pub count: u32,
}
impl Counter {
pub fn new() -> Self {
Counter { count: 0 }
}
}

명령어 정의하기

명령어는 Solana 프로그램이 수행할 수 있는 다양한 작업을 의미합니다. 이를 프로그램의 공개 API로 생각하면 됩니다 - 사용자가 프로그램과 상호작용할 때 취할 수 있는 액션을 정의합니다.

명령어는 일반적으로 Rust enum을 사용하여 정의되며:

  • 각 enum 변형은 서로 다른 명령어를 나타냅니다
  • 변형의 페이로드는 명령어의 매개변수를 나타냅니다

Rust enum 변형은 0부터 시작하여 암시적으로 번호가 매겨진다는 점에 유의하세요.

다음은 두 가지 명령어를 정의하는 enum의 예시입니다:

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

클라이언트가 프로그램을 호출할 때, 다음과 같은 instruction data(바이트 버퍼)를 제공해야 합니다:

  • 첫 번째 바이트는 실행할 명령어 변형을 식별합니다(0, 1 등)
  • 나머지 바이트에는 직렬화된 명령어 매개변수가 포함됩니다(필요한 경우)

instruction data(바이트)를 enum의 변형으로 변환하기 위해 일반적으로 헬퍼 메서드를 구현합니다. 이 메서드는:

  1. 첫 번째 바이트를 분리하여 명령어 변형을 가져옵니다
  2. 변형에 따라 매칭하고 나머지 바이트에서 추가 매개변수를 파싱합니다
  3. 해당하는 enum 변형을 반환합니다

예를 들어, CounterInstruction enum에 대한 unpack 메서드:

impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

카운터 프로그램의 명령어를 정의하기 위해 다음 코드를 lib.rs에 추가하세요.

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,
program_error::ProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?,
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

명령어 핸들러

명령어 핸들러는 각 명령어에 대한 비즈니스 로직을 포함하는 함수를 말합니다. 핸들러 함수의 이름을 process_<instruction_name>로 지정하는 것이 일반적이지만, 원하는 명명 규칙을 자유롭게 선택할 수 있습니다.

다음 코드를 lib.rs에 추가하세요. 이 코드는 이전 단계에서 정의한 CounterInstruction enum과 unpack 메서드를 사용하여 들어오는 명령어를 적절한 핸들러 함수로 라우팅합니다:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,
};
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
// Implementation details...
Ok(())
}
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Implementation details...
Ok(())
}

다음으로, process_initialize_counter 함수의 구현을 추가하세요. 이 명령어 핸들러는:

  1. 카운터 데이터를 저장할 새 계정을 생성하고 공간을 할당합니다
  2. 명령어에 전달된 initial_value로 계정 데이터를 초기화합니다

lib.rs
// Initialize a new counter account
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// Size of our counter account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

다음으로, process_increment_counter 함수의 구현을 추가합니다. 이 명령은 기존 카운터 계정의 값을 증가시킵니다.

lib.rs
// Update an existing counter's value
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
// Verify account ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

명령어 테스트

프로그램 명령어를 테스트하기 위해 다음 종속성을 Cargo.toml에 추가하세요.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

그런 다음 다음 테스트 모듈을 lib.rs에 추가하고 cargo test-sbf를 실행하여 테스트를 수행하세요. 선택적으로 --nocapture 플래그를 사용하여 출력에서 프린트 문을 확인할 수 있습니다.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[cfg(test)]
mod test {
use super::*;
use solana_program_test::*;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[tokio::test]
async fn test_counter_program() {
let program_id = Pubkey::new_unique();
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"counter_program",
program_id,
processor!(process_instruction),
)
.start()
.await;
// Create a new keypair to use as the address for our counter account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_instruction_data.extend_from_slice(&initial_value.to_le_bytes());
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
// Send transaction with initialize instruction
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!(
"✅ Counter initialized successfully with value: {}",
counter.count
);
}
// Step 2: Increment the counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("✅ Counter incremented successfully to: {}", counter.count);
}
}
}

예시 출력:

Terminal
running 1 test
[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago
[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42
[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units
[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43
[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units
[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

목차

페이지 편집