Rust 프로그램 구조
Rust로 작성된 Solana 프로그램은 코드 구성 방식에 유연성을 제공하는 최소한의
구조적 요구사항만 있습니다. 유일한 요구사항은 프로그램에 entrypoint
가 있어야
한다는 것으로, 이는 프로그램 실행이 시작되는 지점을 정의합니다.
프로그램 구조
파일 구조에 대한 엄격한 규칙은 없지만, Solana 프로그램은 일반적으로 다음과 같은 공통 패턴을 따릅니다:
entrypoint.rs
: 들어오는 명령어를 라우팅하는 엔트리포인트를 정의합니다.state.rs
: 프로그램별 상태(계정 데이터)를 정의합니다.instructions.rs
: 프로그램이 실행할 수 있는 명령어를 정의합니다.processor.rs
: 각 명령어에 대한 비즈니스 로직을 구현하는 명령어 핸들러(함수)를 정의합니다.error.rs
: 프로그램이 반환할 수 있는 사용자 정의 오류를 정의합니다.
Solana 프로그램 라이브러리에서 예제를 찾을 수 있습니다.
예제 프로그램
여러 명령어가 있는 네이티브 Rust 프로그램을 구축하는 방법을 보여주기 위해, 두 가지 명령어를 구현하는 간단한 카운터 프로그램을 살펴보겠습니다:
InitializeCounter
: 초기값으로 새 계정을 생성하고 초기화합니다.IncrementCounter
: 기존 계정에 저장된 값을 증가시킵니다.
간단하게 하기 위해 프로그램은 단일 lib.rs
파일에 구현되지만, 실제로는 더 큰
프로그램을 여러 파일로 분할하는 것이 좋을 수 있습니다.
새 프로그램 만들기
먼저, 표준 cargo init
명령어와 --lib
플래그를 사용하여 새 Rust 프로젝트를
만듭니다.
cargo init counter_program --lib
프로젝트 디렉토리로 이동합니다. 기본 src/lib.rs
및 Cargo.toml
파일이 보일
것입니다.
cd counter_program
다음으로, solana-program
의존성을 추가하세요. 이것은 Solana 프로그램을
빌드하는 데 필요한 최소한의 의존성입니다.
cargo add solana-program@1.18.26
다음으로, 아래 코드 조각을 Cargo.toml
에 추가하세요. 이 설정을 포함하지 않으면
프로그램을 빌드할 때 target/deploy
디렉토리가 생성되지 않습니다.
[lib]crate-type = ["cdylib", "lib"]
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
의 기본 코드를 다음 코드로 대체하세요. 이 코드 조각은:
solana_program
에서 필요한 의존성을 가져옵니다entrypoint!
매크로를 사용하여 프로그램 진입점을 정의합니다- 명령어를 적절한 핸들러 함수로 라우팅하는
process_instruction
함수를 구현합니다
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 logicOk(())}
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를 사용하려면:
- "borsh" 크레이트를 "Cargo.toml"의 의존성으로 추가하세요:
[dependencies]borsh = "0.10.3"borsh-derive = "0.10.3"
- 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 0IncrementCounter, // variant 1}
클라이언트가 프로그램을 호출할 때, 다음과 같은 instruction data(바이트 버퍼)를 제공해야 합니다:
- 첫 번째 바이트는 실행할 명령어 변형을 식별합니다(0, 1 등)
- 나머지 바이트에는 직렬화된 명령어 매개변수가 포함됩니다(필요한 경우)
instruction data(바이트)를 enum의 변형으로 변환하기 위해 일반적으로 헬퍼 메서드를 구현합니다. 이 메서드는:
- 첫 번째 바이트를 분리하여 명령어 변형을 가져옵니다
- 변형에 따라 매칭하고 나머지 바이트에서 추가 매개변수를 파싱합니다
- 해당하는 enum 변형을 반환합니다
예를 들어, CounterInstruction
enum에 대한 unpack
메서드:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet 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
에 추가하세요.
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 logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet 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
메서드를 사용하여 들어오는 명령어를 적절한
핸들러 함수로 라우팅합니다:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch 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
함수의 구현을 추가하세요. 이 명령어
핸들러는:
- 카운터 데이터를 저장할 새 계정을 생성하고 공간을 할당합니다
- 명령어에 전달된
initial_value
로 계정 데이터를 초기화합니다
// Initialize a new counter accountfn 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 accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
다음으로, process_increment_counter
함수의 구현을 추가합니다. 이 명령은 기존
카운터 계정의 값을 증가시킵니다.
// Update an existing counter's valuefn 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 ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
명령어 테스트
프로그램 명령어를 테스트하기 위해 다음 종속성을 Cargo.toml
에 추가하세요.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
그런 다음 다음 테스트 모듈을 lib.rs
에 추가하고 cargo test-sbf
를 실행하여
테스트를 수행하세요. 선택적으로 --nocapture
플래그를 사용하여 출력에서 프린트
문을 확인할 수 있습니다.
cargo test-sbf -- --nocapture
#[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 accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_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 instructionlet 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 datalet 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 counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet 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 datalet 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);}}}
예시 출력:
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 successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?