신규: Helius가 Light Protocol을 인수했습니다
Pinocchio로 Solana 스마트 컨트랙트 구축하기
블로그/개발

Pinocchio로 Solana 프로그램을 구축하는 방법

최고의 Solana 개발 에이전시X의 Exo TechnologiesLinkedIn의 Exo Technologies
Exo Technologies 공동 창립자X의 Taylor JohnsonLinkedIn의 Taylor Johnson
읽는 데 12분

Pinocchio는 네이티브 Solana 프로그램을 구축하는 데 사용할 수 있는 고도로 최적화된 무의존성 라이브러리입니다. Solana의 Agave 클라이언트 핵심 개발사인 Anza가 만들었습니다. 

Exo Tech는 Pinocchio를 초기에 도입한 선도적인 Solana 개발 전문 기업입니다. 고객 프로젝트를 통해 Pinocchio를 사용한 여러 프로덕션 프로그램을 개발했으며, 누락된 기능을 추가하기 위해 SDK에도 기여했습니다. 

이 글에서는 Pinocchio로 프로그램을 구축하는 방법과 장점 및 절충점을 자세히 살펴봅니다. 개발자가 자신의 프로그램에 Pinocchio가 적합한지 판단하는 데 필요한 정보를 제공하는 것이 목표입니다. 다만 Pinocchio는 개발자 경험보다 최적화를 우선하므로 초보자에게 친숙하지 않습니다.

Pinocchio 라이브러리란?

Pinocchio 라이브러리는 zero-copy 타입을 광범위하게 활용해 프로그램 실행을 최적화하는 solana-program 크레이트의 대체재입니다. zero-copy를 사용하면 데이터를 읽거나 쓸 때 별도의 메모리 주소로 복사할 필요가 없어 컴퓨팅 리소스, 즉 Solana의 CU를 절약할 수 있습니다.

이 라이브러리는 의존성이 없으며 “no_std”입니다. Rust의 std 크레이트는 운영 체제 리소스에 접근하는 일반적인 방식과 런타임을 제공하지만, Solana Virtual Machine(SVM) 자체가 런타임이므로 이러한 오버헤드는 필요하지 않습니다.

Pinocchio는 solana-program보다 어떻게 더 높은 성능을 제공하나요?

모든 Solana 프로그램에는 런타임이 프로그램 실행을 위해 호출하는 엔트리포인트가 필요합니다. solana-program 라이브러리는 프로그램 입력을 역직렬화하고, 힙 할당자를 설정하며, 패닉 핸들러를 생성하는 entrypoint! 매크로를 제공합니다.

코드
macro_rules! entrypoint {
    ($process_instruction:ident) => {
        /// # Safety
        #[no_mangle]
        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
            let (program_id, accounts, instruction_data) =
                unsafe { $crate::entrypoint::deserialize(input) };
            match $process_instruction(&program_id, &accounts, &instruction_data) {
                Ok(()) => $crate::entrypoint::SUCCESS,
                Err(error) => error.into(),
            }
        }
        $crate::custom_heap_default!();
        $crate::custom_panic_default!();
    };
}

Pinocchio는 세 가지 엔트리포인트 매크로를 제공합니다.

solana-program에서 마이그레이션하는 경우, entrypoint! 매크로는 프로그램 입력을 역직렬화하고 할당자와 핸들러를 설정하므로 대부분 동일하게 작동합니다. 

하지만 나머지 두 매크로는 엔트리포인트를 힙 할당자 및 패닉 핸들러 설정과 분리합니다. 따라서 개발자는 프로그램 로직이 실행되기 전에 이를 생략하거나 최적화할 수 있습니다. 

program_entrypoint!는 solana-program와 유사하게 프로그램 입력을 역직렬화합니다. 반면 lazy_program_entrypoint!는 입력 버퍼만 래핑하고 처리를 프로그램에 위임해 컴퓨팅을 더 세밀하게 제어할 수 있게 합니다. 

이 매크로들은 힙 할당자나 패닉 핸들러를 설정하지 않으므로, Pinocchio 라이브러리는 개발자가 사용할 수 있는 기본 매크로를 제공합니다.

또한 프로그램이 힙 메모리를 전혀 사용하지 않는다는 것을 안다면 no_allocator!를 통해 메모리 할당자 설정을 생략하고 컴퓨팅 유닛(CU)을 절약할 수 있습니다.

Pinocchio 엔트리포인트는 Solana 프로그램 입력을 어떻게 다르게 역직렬화하나요?

앞서 solana-program와 Pinocchio의 엔트리포인트가 프로그램 입력을 역직렬화한다고 간단히 설명했습니다. 하지만 CU를 크게 절감하는 핵심이므로 두 역직렬화 방식의 차이를 이해해야 합니다. 

언뜻 보면 프로그램의 명령어 핸들러로 전달되는 역직렬화된 입력은 동일해 보입니다.

코드
/// solana-program and pinocchio both look the same
process_instruction(
         program_id: &Pubkey,
         accounts: &[AccountInfo],
         instruction_data: &[u8],
     ) -> ProgramResult

핵심 차이는 AccountInfo의 구현 방식에 있습니다. 

solana-program는 데이터를 소유하는 AccountInfo 구조체에 데이터를 쓰지만, Pinocchio의 AccountInfo 구조체는 계정을 나타내는 기본 입력 데이터에 대한 포인터일 뿐입니다. 복사해야 할 데이터가 줄어들어 많은 CU를 절약합니다.

Pinocchio는 개발자가 CU를 최적화하도록 어떻게 지원하나요?

명령어 프로세서가 포인터 참조를 받기 때문에 Pinocchio 라이브러리를 사용하는 개발자의 로직은 작업 중인 데이터를 직접 소유하는 경우가 거의 없습니다. 

이는 AccountInfo의 값에 접근할 때 쉽게 확인할 수 있습니다. key() 메서드로 계정의 공개 키를 읽으면 Pubkey에 대한 참조가 반환됩니다. 따라서 프로그램 실행 전반에서 계정 정보를 더 저렴하게 읽고 계정 데이터를 더 저렴하게 변경할 수 있습니다.

Pinocchio CU 최적화 예시: P-token

최적화를 위해 zero-copy를 계속 활용하는 좋은 예는 p-token 프로그램입니다.

이 프로그램은 표준 SPL Token Program을 대체하도록 작성되었으며, Pinocchio를 사용해 각 트랜잭션의 컴퓨팅 유닛을 크게 줄입니다.

모든 상태에 포인터를 통해 접근한다는 점을 바로 확인할 수 있습니다.

토큰 계정을 역직렬화하는 대신 AccountInfo의 데이터를 확인한 후 포인터를 반환합니다.

각 속성은 함수를 통해 접근하며, 기본형이 아닌 모든 값은 zero-copy를 유지하는 참조를 반환합니다. 

이 방식이 CU 사용량을 크게 줄이는 이유는 CU 최적화 글에서 자세히 확인하세요.

Pinocchio와 Anchor 비교

Anchor는 Solana 프로그램 개발에 널리 쓰이는 규약 중심 프레임워크입니다. AccountInfo 같은 기반 구조를 직접 노출하는 로직이 없으므로 Pinocchio보다 추상화 수준이 높습니다.

대신 Anchor는 앞서 언급한 solana-program 크레이트에 의존하며, 프로그램 개발 과정을 간소화하는 트레이트와 매크로를 제공합니다. Anchor는 명령어 판별자 패턴과 계정 역직렬화 로직을 제공합니다. 역직렬화 로직은 Borsh에 의존합니다. Borsh는 zero-copy가 아니므로 데이터를 다른 메모리 주소로 복사해야 합니다. 

Anchor의 편의성은 Solana 프로그램 개발 속도를 높이지만 CU 사용량이 늘어나는 대가가 따릅니다.

반면 Pinocchio는 개발자가 컴퓨팅 사용량을 세밀하게 조정해야 할 때 solana-program를 대체하도록 설계된 라이브러리입니다. 특정 방식을 강제하지 않으며 개발자가 원하는 형태로 프로그램을 구성할 수 있습니다. Anchor 프로젝트는 구조가 명확히 정의되지만 Pinocchio 프로젝트의 레이아웃은 프로젝트마다 완전히 다를 수 있습니다. 

Pinocchio 라이브러리는 클라이언트 바인딩이나 구현을 처리하지 않습니다. 반면 Anchor는 클라이언트 측에서 프로그램과 상호작용하는 데 사용할 수 있는 IDL 생성을 기본 지원합니다.

Pinocchio를 사용하는 개발자는 직접 작성하거나 Shank, Codama 같은 도구를 사용해야 합니다. 자세한 내용은 아래 Pinocchio 구축을 위한 보조 도구 섹션에서 설명합니다.

Pinocchio와 Steel 비교

Steel은 Solana 프로그램을 작성하는 또 다른 프레임워크입니다. 현재 solana-program 위에 구축되어 있으며, 안전하고 표현력 높은 프로그램을 쉽게 작성할 수 있는 매크로, 함수, 패턴을 제공합니다.

Steel은 규약이 명확해 코드를 읽기 쉬우면서도 모듈성을 유지합니다. 전부 사용해야 하는 Anchor와 달리, 개발자는 Steel에서 필요한 구성 요소만 선택해 사용할 수 있습니다.

Steel의 account! 매크로는 계정 구조를 파싱하는 데 bytemuck을 사용하지만, Pinocchio는 계정 파싱을 전혀 처리하지 않습니다. Steel에는 연결 가능한 파서와 어설션도 포함되어 있어 사용자 지정 검증을 간단히 추가할 수 있습니다. Pinocchio는 이러한 패턴을 기본 제공하지 않으므로 개발자가 직접 검증 패턴을 작성해야 합니다.

하지만 System Program과 Token Program 같은 일반적인 크로스 프로그램 호출(CPI)의 경우 Pinocchio와 Steel 모두 호출을 간편하게 만드는 패턴을 제공합니다.

Pinocchio는 고도로 최적화되어 있지만 모든 세부 사항을 개발자에게 맡깁니다. Steel은 개발자 경험을 개선하도록 설계된 solana-program 라이브러리의 유용한 모듈형 래퍼입니다.

Pinocchio로 토큰을 생성하는 방법

Pinocchio로 작성한 프로그램을 보여주기 위해 Solana 개발자 예제의 토큰 생성 프로그램을 다시 작성하겠습니다.

이 프로그램은 Token2022 토큰 민트를 생성하고 Metadata 토큰 확장을 사용해 토큰 정보를 저장하는 단일 명령어로 구성된 간단한 프로그램입니다. 메타데이터는 이름, 심볼, uri가 포함된 명령어 데이터를 통해 전달됩니다.

1. 엔트리포인트 정의

먼저 프로그램의 엔트리포인트를 정의하겠습니다.

Pinocchio의 기본 할당자와 패닉 처리를 사용하기 위해 전체 엔트리포인트 매크로를 사용합니다.

코드
entrypoint!(process_instruction);

fn process_instruction(
   _program_id: &Pubkey,
   accounts: &[AccountInfo],
   instruction_data: &[u8],
) -> ProgramResult {
   Ok(())
}

2. 명령어 데이터 구조 정의

다음으로 다른 예제 프로그램과 일치하도록 명령어 데이터 구조를 정의합니다. 개발 시간을 줄이기 위해 역직렬화에는 Borsh를 사용하고, 더 최적화된 역직렬화 방법은 다른 글에서 다루겠습니다.

코드
#[derive(BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
   pub name: String,
   pub symbol: String,
   pub uri: String,
   pub decimals: u8,
}

3. 계정 및 명령어 데이터 파싱

이제 명령어 프로세서 내부의 로직을 작성하겠습니다.

먼저 계정 목록에서 계정을 구조 분해하고 명령어 데이터를 CreateTokenArgs로 역직렬화해야 합니다.

코드
let [mint_account, mint_authority, payer, token_program, _system_program] = accounts else {
       return Err(ProgramError::NotEnoughAccountKeys);
   };

   let args = CreateTokenArgs::try_from_slice(instruction_data)
       .map_err(|_| ProgramError::InvalidInstructionData)?;

4. Token2022 민트 계정 생성

계정과 명령어 데이터 파싱을 마쳤으면 System program의 CreateAccount 명령어를 호출합니다.

아래에서는 `pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invoke의 CreateAccount 구조체를 사용합니다.

일반 SPL Token 민트를 생성할 때와 달리, 사용 중인 토큰 확장에 필요한 추가 공간을 계산해야 합니다.

Metadata Pointer 확장의 크기는 고정되어 있지만 Token Metadata 확장은 제공된 인수를 기준으로 동적으로 계산해야 합니다.

코드
 /// [4 (extension discriminator) + 32 (update_authority) + 32 (metadata)]
   const METADATA_POINTER_SIZE: usize = 4 + 32 + 32;
   /// [4 (extension discriminator) + 32 (update_authority) + 32 (mint) + 4 (size of name ) + 4 (size of symbol) + 4 (size of uri) + 4 (size of additional_metadata)]
   const METADATA_EXTENSION_BASE_SIZE: usize = 4 + 32 + 32 + 4 + 4 + 4 + 4;
   /// Padding used so that Mint and Account extensions start at the same index
   const EXTENSIONS_PADDING_AND_OFFSET: usize = 84;

   /* within `process_instruction` */
   let extension_size = METADATA_POINTER_SIZE
       + METADATA_EXTENSION_BASE_SIZE
       + args.name.len()
       + args.symbol.len()
       + args.uri.len();
   let total_mint_size = Mint::LEN + EXTENSIONS_PADDING_AND_OFFSET + extension_size;

   let rent = Rent::get()?;
   // Create the account for the Mint
   CreateAccount {
       from: payer,
       to: mint_account,
       owner: token2022_program.key(),
       lamports: rent.minimum_balance(Mint::LEN),
       space: Mint::LEN as u64,
   }
   .invoke()?;

CreateAccount가 호출되면 SystemProgram이 Token2022 프로그램을 민트 계정의 소유자로 등록합니다.

5. 확장, 계정, 메타데이터 값 초기화

다음으로 Metadata Pointer 확장을 초기화하고, Token2022 프로그램으로 Mint 계정을 초기화하며, 프로그램이 인수로 받은 메타데이터 값을 초기화해 계정 데이터를 설정해야 합니다. 

다음 CPI는 활발히 개발 중인 pinocchio_token 크레이트의 브랜치에서 가져왔습니다. Token2022 기능을 SPL Token 크레이트에서 분리할 예정이므로 이 코드는 곧 오래된 코드가 될 수 있습니다.

코드
// Initialize MetadataPointer extension pointing to the Mint account
   InitializeMetadataPointer {
       mint: mint_account,
       authority: Some(*payer.key()),
       metadata_address: Some(*mint_account.key()),
   }
   .invoke()?;

   // Now initialize that account as a Token2022 Mint
   InitializeMint2 {
       mint: mint_account,
       decimals: args.decimals,
       mint_authority: mint_authority.key(),
       freeze_authority: None,
   }
   .invoke(TokenProgramVariant::Token2022)?;

   // Set the metadata within the Mint account
   InitializeTokenMetadata {
       metadata: mint_account,
       update_authority: payer,
       mint: mint_account,
       mint_authority: payer,
       name: &args.name,
       symbol: &args.symbol,
       uri: &args.uri,
   }
   .invoke()?;

완료되었습니다! 

이제 Pinocchio로 작성된, Token2022 기반의 자체 메타데이터를 포함하는 토큰 민트가 생성되었습니다.

최대한 최적화하려면 이 코드를 더 개선할 수 있지만, 이 예제를 통해 Pinocchio로 프로그램을 작성하는 방법을 이해할 수 있기를 바랍니다.

Pinocchio 구축을 위한 보조 도구

Pinocchio 전용 도구는 아직 많지 않지만 계속 늘고 있습니다.

계정 직렬화 및 역직렬화를 위한 Bytemuck

계정 직렬화 및 역직렬화는 Pinocchio 프로그램 개발자가 직접 구현해야 합니다. 수작업으로 처리하면 번거롭고 오류가 발생하기 쉽습니다. Bytemuck은 바이트 배열을 구조체로 쉽게 읽고 쓸 수 있게 해주는 훌륭한 라이브러리입니다. 메모리에 복사해야 하는 데이터의 양을 제한하므로 상당히 최적화되어 있습니다.

Borsh는 크기가 고정되지 않은 계정을 다룰 때 사용할 수 있는 또 다른 솔루션입니다. 다만 컴퓨팅 효율이 낮으며, 사람들이 Anchor 대신 Pinocchio를 선택하는 이유 중 하나이기도 합니다.

IDL 생성을 위한 Shank

Pinocchio는 라이브러리이므로 Anchor와 같은 내장 IDL 생성 기능이 없습니다. IDL(Interface Definition Language)은 명령어, 계정 구조, 오류 코드를 포함한 Solana 프로그램의 공개 인터페이스를 정의하는 JSON 파일입니다. 표준화된 상호작용을 지원하고 클라이언트 측 개발을 간소화합니다.

IDL 생성에는 Shank를 권장합니다. 이 크레이트를 사용하면 개발자가 코드에 어노테이션을 추가하고 CLI로 유효한 IDL을 매우 쉽게 생성할 수 있습니다. 구조체의 derive 문에 ShankAccount 매크로를 추가하면 해당 구조체가 직렬화 및 역직렬화 가능한 계정임을 나타냅니다. shank CLI를 실행하면 이 구조체가 IDL에 타입이 지정된 계정으로 포함되며, 이후 클라이언트 생성에 사용할 수 있습니다.

또 다른 중요한 매크로는 프로그램의 명령어 enum에 사용하는 ShankInstruction입니다. 이를 통해 #[account] 속성으로 해당 명령어의 계정 목록에서 각 계정의 인덱스와 권한을 지정할 수 있습니다.

Anchor를 사용하지 않는 프로그램의 IDL 생성을 간편하게 만드는 유용한 코드 어노테이션에 관한 자세한 내용은 shank-macro 저장소에서 확인하세요.

클라이언트 생성을 위한 Codama

IDL이 준비되면 Codama로 클라이언트를 쉽게 생성할 수 있습니다. 생성된 코드가 요구 사항에 맞지 않으면 클라이언트를 직접 작성해야 합니다.

Exo Tech에서는 Solana 프로그램 저장소를 빠르게 시작할 수 있도록 Pinocchio 프로젝트 템플릿을 만들었습니다. 자유롭게 사용해 보고 개선 사항이 있다면 풀 리퀘스트를 보내 주세요!

Pinocchio의 미래

Pinocchio는 solana-program의 즉시 교체 가능한 대체재를 목표로 하지만 아직 기능이 동등하지는 않습니다. 일부 sysvar는 아직 지원되지 않으며, 비핵심 크레이트는 완전히 지원되지 않거나 아예 존재하지 않습니다. 예를 들어 Pinocchio Token 프로그램 크레이트는 여러 서명자를 지원하지 않습니다. Token2022 지원도 아직 없지만 개발 중입니다.

Pinocchio 사용의 주요 단점 중 하나는 다른 Solana 프로그램용으로 개발된 모든 SDK가 solana-program 크레이트를 사용한다는 점입니다. 즉, 각 SDK가 AccountInfo 또는 전달되는 데이터를 소유해야 하므로 Pinocchio로 개발한 프로그램과 상호 운용하기가 매우 어렵습니다. 

서드 파티 프로그램과 통합할 때는 각 명령어에 대한 사용자 지정 CPI 로직을 작성해야 하는 경우가 매우 많습니다. Codama 같은 코드 생성기로 결국 해결될 수 있지만 아직은 충분하지 않습니다.

Pinocchio는 여전히 활발히 개발 중이며 감사를 받지 않았다는 점에 유의해야 합니다. 커뮤니티는 나머지 sysvar를 SDK에 포함하고 Token 및 Token2022 같은 주요 SPL 프로그램 지원을 개선하기 위해 계속 작업하고 있습니다.

Pinocchio에 기여하는 방법

Pinocchio에는 쉽게 시작할 수 있는 기여 항목이 많습니다.

추가 지원이 필요한 공개 이슈와 기존 풀 리퀘스트가 있습니다. 논의에 참여하거나 관리자가 검토할 수 있도록 풀 리퀘스트를 보내 주세요!

결론

Pinocchio는 기존 솔루션보다 훨씬 높은 성능으로 Solana 프로그램을 작성할 수 있는 라이브러리입니다. 개발자가 프로그램의 엔트리포인트를 더 유연하게 제어하고 프로그램 입력 접근에 zero-copy를 사용하면 CU 사용량을 줄일 수 있습니다. 하지만 아직 새로운 라이브러리이며 모든 기능이 완성되지는 않았습니다. 이 글을 작성하는 시점에는 감사를 받지 않았으므로 주의해서 사용하세요.

Pinocchio 사용 여부를 평가할 때는 다른 라이브러리 및 프레임워크와의 절충점을 비교해야 합니다.

Anchor 같은 규약 중심 프레임워크는 프로그램 개발 속도를 높이고 유지 관리를 쉽게 해줍니다. 빠른 출시가 중요할 때 탁월한 선택입니다.

제품이 안정화되고 많은 트랜잭션을 처리하게 되면 Pinocchio 같은 라이브러리로 Solana 프로그램을 최적화하는 편이 더 적합할 수 있습니다.

추가 자료

자세한 내용은 Solana Accelerate 2025에서 진행된 Febo의 발표를 시청하고 다음 학습 자료를 살펴보세요.

Helius 구독하기

최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요

확대 이미지