
Anchor 소개: Solana 프로그램 구축을 위한 초보자 가이드
목차
- 이 글에서는 무엇을 다루나요?
- 사전 지식
- Anchor 설치
- Rust 설치
- Solana Tool Suite 설치
- Yarn 설치
- AVM으로 Anchor 설치
- 바이너리로 Anchor 설치 및 소스에서 빌드
- Solana Playground
- Hello, World!
- 로컬 Anchor 환경에서 새 프로젝트 생성
- Solana Playground에서 새 프로젝트 생성
- Hello, World! 작성
- 로컬에서 빌드 및 배포
- Devnet에 배포
- Solana Playground에서 빌드 및 배포
- 효과적인 추상화: IDL과 매크로
- Anchor 프로그램 구조
- 계정 타입
- 계정 제약 조건
- 프로그램의 제약 조건 분석
- 계정 공간
- 오류
- 크로스 프로그램 호출(CPI)
- 권한 상승
- CPI 실행
- 프로그램 파생 주소(PDA)
- 결론
- 추가 자료
이 글을 검토해 주신 Noah, Mike, Jonas, Ryan, Prames, bl0ckpain께 깊이 감사드립니다.
이 글에서는 무엇을 다루나요?
Rust는 흔히 Solana 프로그램 개발의 공용어로 불립니다. 하지만 대부분의 Rust 개발에서 이 프레임워크를 사용하므로 Anchor를 그렇게 표현하는 편이 더 정확합니다. Anchor는 안전한 Solana 프로그램을 빠르게 구축하도록 설계된 강력하고 명확한 원칙을 갖춘 프레임워크입니다. 계정 직렬화 및 역직렬화, 명령어 데이터 등의 상용구 코드를 줄이고, 필수 보안 검사를 수행하며, 클라이언트 라이브러리를 자동 생성하고, 폭넓은 테스트 환경을 제공해 개발 과정을 간소화합니다.
이 글에서는 Anchor 프로그램을 개발하는 방법을 살펴봅니다. Anchor 설치, Solana Playground 사용, 간단한 Hello, World! 프로그램의 생성·빌드·배포를 다룹니다. 이어서 IDL, 매크로, Anchor 프로그램 구조, 계정 유형과 제약 조건, 오류 처리를 살펴보며 Anchor가 개발 과정을 간소화하는 방식을 자세히 알아봅니다. Cross-Program Invocation과 Program Derived Address도 간략히 다룹니다. 지금 Anchor를 시작하는 데 필요한 모든 것을 이 글에서 확인할 수 있습니다.
사전 지식
이 글은 Solana 프로그래밍 모델을 알고 있다고 가정합니다. Solana 개발이 처음이라면 이전 블로그 글인 Solana 프로그래밍 모델: Solana 개발 입문을 먼저 읽어보세요.
Rust가 처음이어도 걱정하지 마세요. Anchor 개발을 시작하는 데 고급 지식은 필요하지 않습니다. Anchor 문서에 따르면 개발자는 Rust의 기초만 익히면 됩니다. 즉, Rust Book의 첫 9개 장에 해당하는 내용입니다. Rust 프로그래밍의 핵심 개념을 알기 쉽게 설명한 The Rust Survival Guide도 시청해 보세요. Rust의 메모리, 소유권, 대여 규칙을 이해하는 것도 매우 중요합니다.
학습 부담을 줄이려면 저수준 프로그래밍 언어가 처음인 개발자는 Rust 자료에서 자주 생략되는 시스템 프로그래밍 관련 개념을 살펴보는 것이 좋습니다. 예를 들어 변수 크기, 포인터, 메모리 누수 같은 주제를 학습해 보세요. 실제 Rust 활용 사례는 Rust By Example과 Rust로 작성한 다양한 자료 구조 및 알고리즘 저장소도 참고하세요.
대신 TypeScript를 사용하고 싶으신가요? Poseidon 프레임워크로 TypeScript를 Rust로 트랜스파일하고 유효한 Anchor 프로그램을 생성하는 TypeScript 기반 Solana 프로그램 작성 방법을 알아보세요.
이 글은 오직 Anchor 개발에만 집중합니다. Native Rust로 프로그램을 개발하는 방법은 다루지 않으며, 관련 지식이 있다고 가정하지도 않습니다. Anchor를 활용한 클라이언트 측 개발도 다루지 않습니다. TypeScript로 Anchor 프로그램을 테스트하고 상호작용하는 방법은 향후 글에서 소개하겠습니다.
이제 Anchor를 시작해 보겠습니다!
Anchor 설치
Anchor 설정은 필요한 도구와 패키지를 설치하는 몇 가지 간단한 단계로 이루어집니다. 이 섹션에서는 Rust, Solana Tool Suite, Yarn, Anchor Version Manager를 설치하는 방법을 다룹니다.
Rust 설치
Rust는 공식 Rust 웹사이트 또는 명령줄에서 설치할 수 있습니다.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shSolana Tool Suite 설치
Anchor에는 Solana Tool Suite도 필요합니다. 이 글을 작성하는 시점의 최신 릴리스인 1.17.16은 macOS와 Linux에서 다음 명령어로 설치할 수 있습니다.
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"Windows 사용자는 다음 명령어로 Solana Tool Suite를 설치할 수 있습니다.
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"하지만 대신 Windows Subsystem for Linux (WSL)을 사용할 것을 강력히 권장합니다. 듀얼 부팅이나 별도의 가상 머신 없이 Windows 컴퓨터에서 Linux 환경을 실행할 수 있습니다. 이 방식을 사용한다면 Linux 설치 안내의 curl 명령어를 따르세요.
개발자는 v1.17.16을 다운로드하려는 버전의 릴리스 태그로 바꿀 수도 있습니다. 또는 stable, beta, edge 채널 이름을 사용하세요. 설치 후 solana –-version을 실행하여 원하는 solana 버전이 설치되었는지 확인합니다.
Yarn 설치
Anchor에는 Yarn도 필요합니다. Node.js 14.9/16.9부터 모든 공식 Node.js 릴리스에 포함된 Corepack을 사용할 수 있습니다. 다만 현재 실험 단계에서는 직접 활성화해야 합니다. 따라서 활성화하려면 **corepack enable**을 실행해야 합니다. 일부 서드 파티 배포판에는 Corepack이 기본으로 포함되지 않을 수 있습니다. 이 경우 corepack enable 전에 **npm install -g corepack**을 실행해야 할 수 있습니다.
AVM으로 Anchor 설치
Anchor 문서에서는 Anchor Version Manager(AVM)를 통해 Anchor를 설치하도록 안내합니다. AVM은 여러 버전의 anchor-cli 바이너리를 관리하고 선택하는 작업을 간소화합니다. 검증 가능한 빌드를 생성하거나 프로그램마다 다른 버전을 사용할 때 필요할 수 있습니다. Cargo에서 cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force 명령어로 설치할 수 있습니다. 그런 다음 최신 버전을 설치하고 사용하세요.
avm install latest
avm use latest
# Verify the installation
avm --version사용 가능한 anchor-cli 버전 목록을 보려면 avm list 명령어를 사용하세요. 특정 버전을 사용하려면 avm use <version>을 사용할 수 있습니다. 변경하기 전까지 해당 버전이 계속 사용됩니다. 특정 버전은 avm uninstall <version> 명령어로 제거할 수 있습니다.
바이너리로 Anchor 설치 및 소스에서 빌드
Linux에서는 npm 패키지 @coral-xyz/anchor-cli를 통해 Anchor 바이너리를 사용할 수 있습니다. 현재는 x86_64 Linux만 지원합니다. 따라서 다른 운영 체제에서는 소스에서 빌드해야 합니다. Cargo를 사용해 CLI를 직접 설치할 수 있습니다. 예시는 다음과 같습니다.
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked다른 Anchor 버전을 설치하려면 --tag 인수를 변경하세요. Cargo 설치에 실패하면 추가 종속 항목을 설치해야 할 수 있습니다. Ubuntu의 예시는 다음과 같습니다.
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev그런 다음 anchor --version 명령어로 Anchor 설치를 확인할 수 있습니다.
Solana Playground
또는 Solana Playground (Solpg)에서 Anchor를 시작할 수도 있습니다. Solana Playground는 Solana 프로그램을 빠르게 개발, 테스트, 배포할 수 있는 브라우저 기반 IDE입니다.
Solana Playground를 처음 사용하는 개발자는 Playground Wallet을 만들어야 합니다. 화면 왼쪽 아래에서 Not connected라고 표시된 빨간색 상태 표시기를 클릭하세요. 다음 모달이 나타납니다.
Continue를 클릭하기 전에 지갑의 키페어 파일을 백업으로 저장하는 것이 좋습니다. Playground Wallet은 브라우저의 로컬 저장소에 저장되기 때문입니다. 브라우저 캐시를 지우면 지갑이 삭제됩니다.
Continue를 클릭하면 IDE에서 바로 사용할 수 있는 devnet 지갑이 생성됩니다.
지갑에 자금을 추가하려면 Playground 터미널에서 solana airdrop <amount> 명령어를 실행하세요. <amount>은 원하는 devnet SOL 수량으로 바꿉니다. 또는 이 faucet에서 devnet SOL을 받을 수 있습니다. devnet SOL을 받는 방법에 관한 가이드도 확인해 보세요.
다음 오류가 발생할 수 있습니다.
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds이는 보통 devnet faucet이 고갈되었거나 너무 많은 SOL을 요청했기 때문입니다. 현재 한도는 5 SOL이며, 이 프로그램을 배포하기에 충분합니다. 따라서 faucet에서 5 SOL을 요청하거나 solana airdrop 5 명령어를 실행하세요. 더 적은 양을 여러 번 요청하면 속도 제한이 적용될 수 있습니다.
Hello, World!
Hello, World! 프로그램은 새로운 프레임워크나 프로그래밍 언어를 익히는 훌륭한 출발점으로 여겨집니다. 간단해서 숙련도와 관계없이 모든 개발자가 이해할 수 있기 때문입니다. 복잡한 로직이나 함수를 도입하지 않고도 새 프로그래밍 모델의 기본 구조와 문법을 보여줍니다. 코딩 입문용 표준 프로그램으로 자리 잡았으므로 Anchor에서도 직접 만들어 보겠습니다. 이 섹션에서는 로컬 Anchor 환경과 Solana Playground에서 Hello, World! 프로그램을 빌드하고 배포하는 방법을 다룹니다.
로컬 Anchor 환경에서 새 프로젝트 생성
Anchor가 설치되어 있다면 새 프로젝트를 만드는 방법은 간단합니다.
anchor init hello-world
cd hello-world이 명령어는 hello-world라는 새 Anchor 프로젝트를 초기화하고 해당 디렉터리로 이동합니다. 이 디렉터리에서 hello-world/programs/hello-world/src/lib.rs로 이동하세요. 이 파일에는 다음 시작 코드가 들어 있습니다.
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
pub mod hello-world {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}Anchor가 여러 파일과 디렉터리를 준비했습니다. 구체적으로는 다음과 같습니다.
- 프로그램 클라이언트를 위한 빈 app
- 모든 Solana 프로그램이 저장되는 programs 폴더
- JavaScript 테스트를 위한 tests 폴더. 시작 코드용 테스트 파일이 자동으로 생성되어 있습니다
- Anchor.toml 구성 파일. Rust가 처음이라면 TOML 파일은 의미 구조 덕분에 읽기 쉬운 최소 구성 파일 형식입니다. Anchor.toml 파일은 Anchor가 프로그램과 상호작용하는 방식을 구성합니다. 예를 들어 프로그램을 배포할 클러스터를 지정합니다.
Solana Playground에서 새 프로젝트 생성
Solana Playground에서 새 프로젝트를 만드는 과정은 매우 간단합니다. 왼쪽 위로 이동하여 Create a New Project를 클릭하세요.
다음 모달이 나타납니다.
프로그램 이름을 입력하고 **Anchor(Rust)**를 선택한 후 Create를 클릭하세요. 브라우저에서 바로 새 Anchor 프로젝트가 생성됩니다. 왼쪽 Program 섹션에 src 디렉터리가 표시됩니다. 이 디렉터리에는 다음 시작 코드가 포함된 lib.rs가 있습니다.
use anchor_lang::prelude::*;
// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");
#[program]
mod hello_anchor {
use super::*;
pub fn initialize(ctx: Context, data: u64) -> Result<()> {
ctx.accounts.new_account.data = data;
msg!("Changed data to: {}!", data); // Message will show up in the tx logs
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
// We must specify the space in order to initialize an account.
// First 8 bytes are default account discriminator,
// next 8 bytes come from NewAccount.data being type u64.
// (u64 = 64 bits unsigned integer = 8 bytes)
#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct NewAccount {
data: u64
}Solana Playground는 client.ts와 anchor.test.ts 파일만 생성합니다. 새 Anchor 프로젝트에서 일반적으로 생성되는 항목을 자세히 알아보려면 로컬에서 Anchor 프로그램을 만드는 섹션을 읽어보세요.
Hello, World! 작성
로컬 Anchor와 Solana Playground 중 어느 것을 사용하든 매우 간단한 Hello, World! 프로그램을 만들려면 시작 코드를 다음 코드로 바꾸세요.
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
mod hello_world {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
msg!("Hello, World!");
Ok(())
}
#[derive(Accounts)]
pub struct Hello {}
}각 부분의 구체적인 내용은 다음 섹션에서 설명하겠습니다. 지금은 매크로와 트레이트를 사용해 개발 과정을 간소화한다는 점이 중요합니다. declare_id! 매크로는 프로그램의 공개 키를 설정합니다. 로컬 개발에서는 프로그램을 설정하는 anchor init 명령어가 target/deploy 디렉터리에 키페어를 생성하고 이 매크로를 채웁니다. Solana Playground에서도 자동으로 처리됩니다.
기본 hello_world 모듈에서는 **Hello, World!**를 기록하는 함수를 만듭니다. 또한 프로그램이 성공적으로 실행되었음을 나타내기 위해 Ok(())을 반환합니다. 콘솔에 사용되지 않은 변수 경고가 표시되지 않도록 ctx 앞에 밑줄을 붙였습니다. Hello는 계정 구조체입니다. 이 프로그램은 새 메시지만 기록하므로 전달할 계정이 필요하지 않습니다.
이것이 전부입니다! 계정을 받거나 복잡한 로직을 구현할 필요가 없습니다. 위 코드는 Hello, World!를 기록하는 프로그램을 만듭니다.
로컬에서 빌드 및 배포
이 섹션에서는 Localhost 배포에 집중합니다. Solana Playground의 기본값은 devnet이지만, 로컬 개발 환경은 훨씬 나은 개발자 경험을 제공합니다. 더 빠를 뿐 아니라 devnet 테스트에서 흔히 발생하는 여러 문제를 피할 수 있습니다. 예를 들면 트랜잭션을 위한 SOL 부족, 느린 배포, devnet 중단 시 테스트 불가 등의 문제가 있습니다. 반면 로컬 개발에서는 테스트할 때마다 초기 상태를 보장할 수 있습니다. 더 통제되고 효율적인 개발 환경을 구축할 수 있습니다.
도구 구성
먼저 Solana Tool Suite가 Localhost 개발용으로 올바르게 구성되었는지 확인합니다. solana config set --url localhost 명령어를 실행하여 모든 구성이 Localhost URL을 가리키는지 확인하세요.
또한 로컬에서 Solana와 상호작용할 로컬 키페어가 있는지 확인하세요. Solana CLI로 프로그램을 배포하려면 SOL 잔액이 있는 Solana 지갑이 필요합니다. solana address 명령어를 실행해 로컬 키페어가 이미 있는지 확인하세요. 오류가 발생하면 solana-keygen new 명령어를 실행합니다. 기본적으로 ~/.config/solana/id.json 경로에 새 파일 시스템 지갑이 생성됩니다. 공개 키와 비공개 키를 복구할 수 있는 복구 문구도 제공됩니다. 로컬에서만 사용하더라도 이 키페어를 저장해 두는 것이 좋습니다. 기본 위치에 파일 시스템 지갑이 이미 있다면 --force 명령어로 별도 지정하지 않는 한 solana-keygen new 명령어가 이를 덮어쓰지 않습니다.
Anchor.toml 구성
다음으로 Anchor.toml 파일이 Localhost를 올바르게 가리키는지 확인합니다. 다음 코드가 포함되어 있어야 합니다.
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'여기서 [programs.localnet]은 localnet, 즉 Localhost에 있는 프로그램의 ID를 나타냅니다. 프로그램 ID는 항상 클러스터를 기준으로 지정됩니다. 같은 프로그램이라도 다른 클러스터에서는 다른 주소에 배포할 수 있기 때문입니다. 개발자 경험 측면에서 여러 클러스터에 배포된 프로그램마다 새 프로그램 ID를 선언하는 작업은 번거로울 수 있습니다.
프로그램 ID는 공개됩니다. 하지만 키페어는 target/deploy 폴더에 저장됩니다. 프로그램 이름을 기반으로 한 특정 명명 규칙을 따릅니다. 예를 들어 프로그램 이름이 hello_world라면 Anchor는 target/deploy/hello-world-keypair.json에서 키페어를 찾습니다. 배포 중 이 파일을 찾지 못하면 Anchor가 새 키페어를 생성합니다. 그러면 새 프로그램 ID가 만들어집니다. 따라서 첫 배포 후 프로그램 ID를 업데이트하는 것이 중요합니다. hello-world-keypair.json 파일은 프로그램 소유권의 증명으로 사용됩니다. 키페어가 유출되면 악의적인 사용자가 프로그램을 무단으로 변경할 수 있습니다.
[provider]을 통해 Anchor가 Localhost와 지정된 지갑을 사용해 저장 공간 및 트랜잭션 비용을 지불하도록 설정합니다.
로컬 원장 빌드, 배포 및 실행
anchor build 명령어로 프로그램을 빌드하세요. 이름으로 특정 프로그램을 빌드하려면 anchor build -p <program name> 명령어를 사용하고 <program name>을 프로그램 이름으로 바꿉니다. localnet에서 개발 중이므로 Anchor CLI의 localnet 명령어로 개발 과정을 간소화할 수 있습니다. 예를 들어 anchor localnet --skip-build은 워크스페이스의 프로그램 빌드를 건너뛸 때 특히 유용합니다. 프로그램 코드가 변경되지 않았다면 테스트 실행 시간을 줄일 수 있습니다.
지금 anchor deploy 명령어를 실행하면 오류가 발생합니다. 테스트할 Solana 클러스터가 로컬 컴퓨터에서 실행되고 있지 않기 때문입니다. 로컬 원장을 실행해 컴퓨터에서 클러스터를 시뮬레이션할 수 있습니다. Solana CLI에는 테스트 검증인이 기본으로 포함되어 있습니다. solana-test-validator 명령어를 실행하면 워크스테이션에서 모든 기능을 갖춘 단일 노드 클러스터가 시작됩니다. RPC 속도 제한과 에어드롭 한도가 없고, 온체인 프로그램을 직접 배포할 수 있으며, 파일에서 계정을 불러오고 공개 클러스터에서 계정을 복제할 수 있다는 장점이 있습니다. localhost 클러스터를 온라인으로 유지하고 상호작용하려면 테스트 검증인을 별도의 열린 터미널 창에서 계속 실행해야 합니다.
이제 anchor deploy을 실행해 프로그램을 로컬 원장에 배포할 수 있습니다. 로컬 원장으로 전송된 모든 데이터는 현재 작업 디렉터리에 생성되는 test-ledger 폴더에 저장됩니다. 이 폴더가 저장소에 커밋되지 않도록 .gitignore 파일에 추가하는 것이 좋습니다. 또한 로컬 원장을 종료해도, 즉 터미널에서 Ctrl + C를 눌러도 클러스터로 전송된 데이터는 삭제되지 않습니다. 데이터를 삭제하려면 test-ledger 폴더를 제거하거나 solana-test-validator --reset을 실행하세요.
축하합니다! 첫 Solana 프로그램을 Localhost에 배포했습니다!
Solana Explorer
개발자는 로컬 원장에 맞게 Solana Explorer를 구성할 수도 있습니다. Solana Explorer로 이동하세요. 탐색 모음에서 현재 클러스터를 표시하는 녹색 버튼을 클릭합니다.
클러스터를 선택할 수 있는 사이드바가 열립니다. Custom RPC URL을 클릭하세요. http://localhost:8899가 자동으로 입력되어야 합니다. 그렇지 않다면 explorer가 컴퓨터의 8899번 포트를 가리키도록 직접 입력하세요.
이는 여러 면에서 매우 유용합니다.
- 개발자는 일반적으로 devnet이나 mainnet을 분석하는 블록 explorer에서 제공되는 기능처럼 로컬 원장의 트랜잭션을 실시간으로 검사할 수 있습니다
- 실제 클러스터에서 작동하는 것처럼 계정, 토큰, 프로그램의 상태를 더 쉽게 시각화할 수 있습니다
- 오류와 트랜잭션 실패에 관한 상세 정보를 제공합니다
- 익숙한 인터페이스를 사용하므로 클러스터 전반에서 일관된 개발 경험을 제공합니다
Devnet에 배포
Localhost 개발을 권장하지만, 특정 클러스터에서 테스트하려는 개발자는 devnet에 배포할 수도 있습니다. 과정은 대체로 같지만 로컬 원장을 실행할 필요는 없습니다. 상호작용할 수 있는 완전한 Solana 클러스터가 이미 있기 때문입니다.
solana config set --url devnet 명령어를 실행해 선택한 클러스터를 devnet으로 변경하세요. 이제 터미널에서 실행하는 모든 solana 명령어는 devnet에서 실행됩니다. 그런 다음 Anchor.toml 파일에서 [programs.localnet] 섹션을 복제하고 이름을 [programs.devnet]으로 변경하세요. [provider]도 devnet을 가리키도록 변경합니다.
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'프로그램을 배포하려면 devnet SOL이 있어야 합니다. solana airdrop <amount> 명령어로 ~/.config/solana/id.json의 기본 키페어 위치에 에어드롭하세요. solana aidrop <amount> <wallet address>을 사용해 지갑 주소를 지정할 수도 있습니다. 또는 이 faucet에서 devnet SOL을 받을 수 있습니다. devnet SOL을 받는 방법에 관한 가이드도 확인해 보세요.
다음 오류가 발생할 수 있습니다: 트랜잭션을 확인할 수 없습니다. 트랜잭션 만료 또는 수수료 지불자의 자금 부족 등의 상황에서 발생할 수 있습니다
이는 보통 devnet faucet이 고갈되었거나 한 번에 너무 많은 SOL을 요청했기 때문입니다. 현재 한도는 5 SOL이며, 이 프로그램을 배포하기에 충분합니다. 따라서 faucet에서 5 SOL을 요청하거나 solana airdrop 5 명령어를 실행하세요. 더 적은 양을 여러 번 요청하면 속도 제한이 적용될 수 있습니다.
이제 다음 명령어로 프로그램을 빌드하고 배포하세요.
anchor build
anchor deploy축하합니다! 로컬 환경에서 첫 Solana 프로그램을 devnet에 배포했습니다!
Solana Playground에서 빌드 및 배포
Solana Playground에서 왼쪽 사이드바의 Tools 아이콘으로 이동합니다. Build를 클릭하세요. 콘솔에 다음 내용이 표시됩니다.
Building...
Build successful. Completed in 2.20s..declare_id! 매크로의 ID가 덮어써진 것을 확인하세요. 이 새 주소에 프로그램을 배포합니다. 이제 Deploy를 클릭하세요. 콘솔에 다음과 비슷한 내용이 표시됩니다.
Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17s축하합니다! Solana Playground를 통해 첫 Solana 프로그램을 devnet에 배포했습니다!
효과적인 추상화: IDL과 매크로
Anchor는 효과적인 추상화를 통해 프로그램 개발을 간소화합니다. 즉, 복잡한 블록체인 프로그래밍 개념을 더 쉽게 이해하고 다룰 수 있도록 합니다. 예를 들어 Anchor는 인터페이스 정의 언어(IDL)를 사용해 프로그램의 인터페이스를 정의합니다. 프로그램을 빌드하면 Anchor가 프로그램의 IDL을 나타내는 JSON 파일을 생성합니다. 이 구조는 클라이언트 측에서 프로그램의 함수 및 데이터 구조와 상호 작용하는 방식을 정의하는 데 사용할 수 있습니다. Anchor는 상태 관리를 위한 고수준 추상화도 제공합니다. 개발자는 원시 바이트 배열이나 수동 직렬화보다 직관적인 Rust 구조체로 프로그램의 상태를 정의할 수 있습니다. 따라서 일반적인 Rust 데이터 구조를 사용할 때처럼 상태를 정의하면 Anchor가 기본 직렬화와 계정 저장을 처리합니다.
IDL을 온체인에 게시하는 과정도 매우 간단합니다. 다음 명령어로 IDL을 게시할 수 있습니다.
anchor idl init --filepath --provider.cluster --provider.wallet제공한 지갑이 프로그램의 권한을 보유하고 있으며 트랜잭션에 필요한 SOL이 충분한지 확인하세요. 이제 Orb와 같은 블록 탐색기에서 IDL을 확인할 수 있습니다.
예를 들어 Orb에서 DFlow의 aggregator v4 IDL을 확인할 수 있습니다.
Anchor의 매크로는 가장 중요한 추상화 중 하나입니다. Rust에서 매크로는 다른 코드를 생성하는 코드 조각입니다. 이는 메타프로그래밍의 한 형태입니다. 선언적 매크로는 Rust에서 가장 널리 사용되는 매크로 형식입니다. 개발자는 macro_rules! 구문을 통해 match 표현식과 유사한 코드를 작성할 수 있습니다. 절차적 매크로는 함수에 더 가깝게 동작합니다. 코드를 입력으로 받아 처리한 후 결과를 생성합니다. 예를 들어 Anchor에서 #[account] 매크로는 Solana 계정의 제약 조건을 정의하고 적용합니다. 이를 통해 계정 관리의 복잡성과 잠재적 오류를 줄일 수 있습니다. Anchor의 매크로를 살펴보려면 프로그램 구조도 함께 이해해야 합니다.
Anchor 프로그램 구조
Anchor의 프로그램 구조는 매크로와 트레이트를 함께 활용해 상용구 코드를 생성하고 프로그램 로직을 적용하도록 설계되었습니다. 이러한 설계 철학은 개발 프로세스를 간소화하고 프로그램 동작의 일관성과 안정성을 보장하는 데 중요한 역할을 합니다.
use 선언은 파일 상단에 있습니다. 이는 일반적인 Rust 언어 의미론이며 Anchor에만 해당하지 않습니다. 이 선언은 다른 경로와 동일한 하나 이상의 로컬 이름 바인딩을 생성합니다. 즉, use 선언은 모듈 항목을 참조하는 데 필요한 경로를 줄여 줍니다. 모듈이나 블록에 사용할 수 있습니다. 또한 self 키워드는 공통 접두사와 공통 상위 모듈을 가진 경로 목록을 바인딩할 수 있습니다. 예를 들어 다음은 모두 유효한 use 선언입니다.
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};
use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};개발자가 처음 접하게 되는 Anchor 매크로는 declare_id!입니다. 프로그램 주소, 즉 프로그램 ID를 선언해 모든 상호 작용이 해당 프로그램으로 올바르게 라우팅되도록 합니다. 개발자가 Anchor 프로그램을 처음 빌드하면 Anchor가 새 키페어를 생성합니다. 별도로 지정하지 않는 한 이 키페어가 프로그램 배포에 사용됩니다. 키페어의 공개 키를 declare_id! 매크로의 프로그램 ID로 제공해야 합니다.
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");#[program] 속성 매크로는 프로그램의 명령어 로직 모듈을 나타냅니다. 진입점 역할을 하며 프로그램이 수신한 명령어를 해석하고 실행하는 방식을 정의합니다. 이 매크로는 명령어를 프로그램 내 적절한 함수로 라우팅하는 과정을 간소화해 코드를 더 체계적이고 관리하기 쉽게 만듭니다. 이 모듈의 각 함수는 별도의 명령어로 취급됩니다. 각 함수는 첫 번째 인수로 Context 타입의 컨텍스트 매개변수(ctx)를 받습니다. 개발자는 계정, 실행 중인 프로그램의 프로그램 ID, 나머지 계정에 접근할 수 있습니다.
Context 타입은 다음과 같이 정의됩니다.
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
pub program_id: &'a Pubkey,
pub accounts: &'b mut T,
pub remaining_accounts: &'c [AccountInfo<'info>],
pub bumps: T::Bumps,
}이는 주어진 프로그램에 인수가 아닌 입력을 제공하는 데 도움이 됩니다. program_id 필드는 Pubkey 타입이며 현재 실행 중인 프로그램 ID를 나타냅니다. accounts는 직렬화된 계정을 가리키고, remaining_accounts는 제공되었지만 역직렬화되거나 검증되지 않은 나머지 계정을 가리킵니다. 이를 직접 사용할 때는 각별히 주의하세요. bumps 필드는 #[derive(Accounts)]에서 생성한 Bumps 타입입니다. 제약 조건 검증 중 발견된 범프 시드를 나타냅니다. 계정 제약 조건은 이후 섹션에서 다룹니다. 지금은 핸들러가 범프 시드를 다시 계산하거나 인수로 전달하지 않아도 되도록 편의를 위해 제공된다는 점만 알아두면 됩니다.
Context는 제네릭 타입입니다. Rust의 제네릭을 사용하면 모든 데이터 타입에서 동작하는 유연하고 재사용 가능한 코드를 작성할 수 있습니다. 제네릭은 실제로 사용할 정확한 타입을 지정하지 않고도 구조체, 열거형, 함수, 메서드의 타입을 정의할 수 있게 합니다. 대신 일반적으로 T로 표시하는 플레이스홀더를 해당 타입에 사용합니다. 제네릭은 반복 코드를 줄이고 명확성을 높입니다. 예를 들어 제네릭 데이터 타입을 담는 열거형을 다음과 같이 정의할 수 있습니다.
enum Option<T> {
Some(T),
None,
}위 코드 스니펫은 Option<T> 열거형을 보여 줍니다. 모든 타입의 값(즉, Some(T))을 담거나 타입이 없는 상태(None)를 나타낼 수 있는 표준 Rust 열거형입니다.
여기서 Context는 제네릭 타입이며, T는 명령어에 필요한 계정, 즉 개발자가 데이터를 저장하기 위해 생성하려는 타입을 지정합니다. Context를 사용할 때 개발자는 T를 Accounts 트레이트를 구현하는 구조체로 정의할 수 있습니다. 예를 들면 Context<SetData>입니다. 개발자는 점 표기법으로 Context 타입의 필드에 접근할 수 있습니다. 예를 들어 ctx.accounts는 Context 구조체의 accounts 필드에 접근합니다.
앞서 설명했듯이 #[account] 매크로는 사용자 지정 계정 타입을 정의합니다. 다음 섹션에서는 #[account(...)]를 사용해 계정 타입과 제약 조건을 살펴봅니다. 지금은 Accounts 구조체에서 명령어가 요구하는 계정과 해당 계정이 따라야 할 제약 조건을 정의한다는 점이 중요합니다.
계정 타입
Account 타입은 명령어가 계정의 역직렬화된 데이터에 접근할 때 사용합니다. Account 구조체는 T에 대한 제네릭이며 다음과 같이 정의됩니다.
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }이는 프로그램 소유권을 검증하고 기본 데이터를 Rust 타입으로 역직렬화하는 AccountInfo의 래퍼입니다. Account.info.owner == T::owner() 조건으로 프로그램 소유권을 확인합니다. 즉, 데이터 소유자가 #[account]를 사용하는 크레이트의 ID, 즉 앞서 declare_id!로 생성한 값과 동일한지 확인합니다. 따라서 Account가 래핑하는 데이터 타입(=T)은 Owner 트레이트를 구현해야 합니다. #[account] 속성은 동일한 프로그램에서 declare_id!로 선언한 crate::ID를 사용해 구조체에 이 트레이트를 구현합니다. 대부분의 경우 개발자는 #[account] 속성만 사용해 데이터에 필요한 트레이트와 구현을 추가할 수 있습니다. #[account] 속성은 다음 트레이트의 구현을 생성합니다.
계정 직렬화를 위한 트레이트를 구현할 때 처음 8바이트는 고유한 계정 판별자에 할당됩니다. 이 판별자는 계정의 Rust 식별자를 SHA-256으로 해시한 값의 처음 8바이트로 결정됩니다. AccountDeserialize의 try_deserialize를 호출하면 이 판별자를 확인하며, 유효하지 않은 계정이 제공된 경우 오류와 함께 계정 역직렬화를 중단합니다.
개발자가 Anchor가 아닌 프로그램과 상호 작용해야 하는 경우도 있습니다. 이때 #[account]를 사용하는 대신 사용자 지정 래퍼 타입을 만들면 Account의 모든 이점을 활용할 수 있습니다. 다음 코드 스니펫을 예로 살펴보겠습니다.
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
// Rest of the program
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
#[account(
constraint = my_account.mint == token_account.mint,
has_one = owner
)]
pub token_account: Account<'info, TokenAccount>,
pub owner: Signer<'info>
}대부분의 계정 검증은 다음 섹션에서 다룰 계정 제약 조건을 통해 수행됩니다. 여기서는 TokenAccount 타입을 사용해 수신 계정이 토큰 프로그램의 소유인지 확인하는 방식에 주목하세요. TokenAccount는 토큰 프로그램의 Account 구조체를 래핑하고 필요한 함수를 추가합니다. 이를 통해 Anchor가 계정을 역직렬화할 수 있으며, 개발자는 계정 제약 조건과 명령어 함수에서 해당 필드를 사용할 수 있습니다.
또한 위 코드 스니펫에서 derive 매크로가 전체 구조체를 감싼다는 점에 유의하세요. 이 매크로는 SetData에 Accounts 역직렬화 기능을 구현하며 수신 계정을 검증하는 데 사용됩니다.
계정 검증 구조체에서 사용할 수 있는 Account 타입은 다음과 같습니다.
- Account<’info, T>: 역직렬화할 때 소유권을 확인하는 계정 컨테이너
- AccountInfo<’info>: 타입으로 사용할 수 있는 미검사 계정입니다. 하지만 AccountInfo는 향후 릴리스에서 사라질 가능성이 있으므로 UncheckedAccount를 대신 사용해야 합니다
- AccountLoader<’info, T>: 필요할 때 제로 카피 역직렬화를 수행할 수 있게 하는 타입입니다.
Account사용 방식과 달리, 계정을 초기화한 후에는load_init, 계정이 변경 불가능할 때는load, 계정이 변경 가능할 때는load_mut를 호출해야 합니다 - Box<Account<’info, T>> 또는 Box<InterfaceAccount<’info, T>>: 스택 공간을 절약하는 박스 타입입니다. 계정이 스택에 담기에는 너무 커서 스택 위반이 발생할 수 있을 때 계정을 박싱하면 문제를 해결하는 데 도움이 됩니다
- Interface<’info, T>:
Program를 래핑해 계정이 주어진 프로그램 집합 중 하나인지 검증하는 타입입니다. 예상 프로그램에 계정의 키가 포함되어 있는지, 계정이 실행 가능한지 확인합니다 - InterfaceAccount<’info, T>: 프로그램 소유권을 확인하고 기본 데이터를 Rust 타입으로 역직렬화하는 계정 컨테이너
- Option<Account<’info, T>>: 선택적 계정을 위한 옵션 타입
- Program<’info, T>: 계정이 지정된 프로그램인지 검증하는 타입
- Signer<’info>: 계정이 트랜잭션에 서명했는지 검증하는 타입
- SystemAccount<’info>: 계정이 System Program의 소유인지 검증하는 타입
- Sysvar<’info, T>: 계정이 sysvar인지 검증하는 타입입니다. 즉, 네트워크 클러스터, 블록체인 기록, 실행 중인 트랜잭션에 관해 동적으로 업데이트되는 데이터를 담는 특수 타입인지 확인합니다.
clock,epoch_schedule,instructions,rentsysvar는 프로그램 개발에 유용합니다 - UncheckedAccount<’info>: 지정된 계정에 아무런 검사가 수행되지 않음을 명확히 강조하는 계정 컨테이너
계정 제약 조건
안전한 Anchor 프로그램을 개발하려면 계정 제약 조건이 필수적입니다. 향후 글에서는 Solana 프로그램 보안과 Anchor 프로그램 해킹을 더 자세히 다룰 예정입니다. 하지만 여기서도 제약 조건을 살펴볼 필요가 있습니다. 제약 조건을 사용하면 특정 계정이나 해당 계정의 데이터가 미리 정의된 요구 사항과 일치하는지 확인할 수 있습니다. #[account(...)] 속성으로 여러 유형의 제약 조건을 적용할 수 있으며, 다른 데이터 구조를 참조할 수도 있습니다. 형식은 다음과 같습니다.
#[account(constraint goes here)]
pub account: AccountType또한 Accounts 매크로 내에서 #[instruction(...)] 속성을 사용해 명령어 인수에 접근할 수 있습니다. 명령어에 정의된 순서와 동일하게 인수를 나열해야 하지만, 필요한 마지막 인수 뒤에 오는 인수는 모두 생략할 수 있습니다. Anchor 문서의 다음 예시를 살펴보세요.
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
...
Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
...
}계정 제약 조건은 일반 제약 조건과 SPL 제약 조건으로 나눌 수 있습니다. 이 글의 나머지 부분에서 구체적인 제약 조건을 살펴봅니다. 예시에서 <expr>는 예상 타입의 값으로 평가되기만 하면 전달할 수 있는 임의의 표현식을 나타냅니다. 예를 들면 owner = token_program.key()입니다.
프로그램의 제약 조건 분석
가능한 제약 조건의 전체 목록은 계정에 관한 Anchor 문서를 참고하는 것이 좋습니다. 모든 제약 조건을 표 형식으로 일일이 나열하고 공식 정의를 제공하는 것은 지나치게 복잡합니다. 여기서는 다음 프로그램을 분석해 계정 제약 조건이 실제로 어떻게 동작하는지 파악하는 편이 더 유용합니다.
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};
declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");
pub mod errors;
pub mod instructions;
pub mod state;
pub use instructions::*;
pub use state::*;
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "Fanout",
project_url: "http://helium.com",
contacts: "email:hello@helium.foundation",
policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",
// Optional Fields
preferred_languages: "en",
source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
source_revision: default_env!("GITHUB_SHA", ""),
source_release: default_env!("GITHUB_REF_NAME", ""),
auditors: "Sec3"
}
#[program]
pub mod fanout {
use super::*;
pub fn initialize_fanout_v0(
ctx: Context<InitializeFanoutV0>,
args: InitializeFanoutArgsV0,
) -> Result<()> {
instructions::initialize_fanout_v0::handler(ctx, args)
}
pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
instructions::stake_v0::handler(ctx, args)
}
pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
instructions::unstake_v0::handler(ctx)
}
pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
instructions::distribute_v0::handler(ctx)
}
}이 코드는 Helium의 Fanout 프로그램입니다. 보유량에 비례해 토큰 보유자에게 토큰을 분배하는 상당히 복잡한 프로그램입니다. 현재 상태만 보면 제약 조건이 없어 별다른 정보를 얻기 어렵습니다. 하지만 stake_v0 명령어의 StakeV0 구조체를 분석하면 다양한 제약 조건을 살펴볼 수 있습니다.
mut
이 명령어의 첫 번째 제약 조건은 mut 계정 제약 조건입니다. mut는 #[account(mut)] 또는 #[account(mut @ <custom_error>)]로 정의되며 @ 표기법을 통한 사용자 지정 오류를 지원합니다. 이 제약 조건은 주어진 계정이 변경 가능한지 확인하고 Anchor가 모든 상태 변경을 유지하도록 합니다. Helium 프로그램에서는 payer 계정이 변경 가능하도록 보장합니다.
...
pub struct StakeV0<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub staker: Signer<'info>,
/// CHECK: Just needed to receive nft
pub recipient: AccountInfo<'info>,
...has_one
has_one 제약 조건은 #[account(has_one = <target_account)] 또는 #[account(has_one = <target_account> @ <custom_error>)]로 정의됩니다. target_account 필드를 확인해 계정이 Accounts 구조체에 있는 target_account 필드의 키와 일치하는지 검사합니다. @ 주석을 통해 사용자 지정 오류를 지원합니다.
StakeV0 구조체의 컨텍스트에서 has_one 제약 조건은 계정에 membership_mint, token_account, membership_collection가 있는지 확인하는 데 사용됩니다.
...
#[account(
mut,
has_one = membership_mint,
has_one = token_account,
has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...여러 has_one 제약 조건과 mut 제약 조건을 함께 사용하고 있다는 점에 주목하세요. 하나의 계정에 여러 계정 제약 조건을 동시에 적용할 수 있습니다.
seeds, bump
seeds 및 bump 제약 조건은 주어진 계정이 현재 실행 중인 프로그램, 시드, 그리고 제공된 경우 범프로부터 파생된 PDA인지 확인하는 데 사용됩니다.
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
범프를 제공하지 않으면 Anchor가 정식 범프를 사용합니다. Seeds::program = <expr>를 사용하면 현재 실행 중인 프로그램이 아닌 다른 프로그램에서 PDA를 파생할 수 있습니다.
Helium의 fanout 프로그램에서 seeds 제약 조건은 텍스트 “metadata”, token_metadata_program 키, membership_collection 키, 텍스트 “edition”이 이 PDA를 파생하는 데 사용된 시드인지 확인합니다. seeds::program 제약 조건은 현재 프로그램 대신 token_metadata_program를 사용해 PDA를 파생하도록 보장합니다.
...
#[account(
mut,
seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
seeds::program = token_metadata_program.key(),
bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...token::mint, token::authority
token::mint 및 token::authority 제약 조건은 다음과 같이 정의됩니다.
#[account(token::mint = <target account>, token::authority = <target account>)]#[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]
mint 및 authority 토큰 제약 조건은 TokenAccount의 민트 주소와 권한을 검증하는 데 사용됩니다. 이 제약 조건은 검사 용도로 사용하거나 init 제약 조건과 함께 사용해 지정된 민트 주소와 권한을 가진 토큰 계정을 생성할 수 있습니다. 검사 용도로 사용할 때는 제약 조건의 일부만 지정할 수도 있습니다.
Helium 프로그램의 컨텍스트에서 이 제약 조건은 associated_token의 민트가 membership_mint와 같은지, 토큰의 권한이 staker로 설정되어 있는지 확인하는 데 사용됩니다.
...
#[account(
mut,
associated_token::mint = membership_mint,
associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...init, payer, space
이제 코드의 뒷부분으로 이동해 init, payer, space 제약 조건을 분석하겠습니다. init 제약 조건은 [#account(init, payer = <target_account>, space = <num_bytes>)]로 정의됩니다. 이 제약 조건은 System Program에 대한 CPI를 통해 계정을 생성하고 계정 판별자를 설정해 초기화합니다. 계정은 변경 가능한 것으로 표시되며 mut와 함께 사용할 수 없습니다. 10 키비바이트보다 큰 계정에는 #[account(zero)]를 사용하세요.
init 제약 조건은 몇 가지 추가 제약 조건과 함께 사용해야 합니다. 계정 생성 비용을 지불할 계정을 지정하는 payer 제약 조건이 필요합니다. 또한 구조체에 System Program이 있어야 하며 이름은 system_program이어야 합니다. space 제약 조건도 정의해야 합니다. 계정 공간 섹션에서 이 제약 조건과 공간 요구 사항을 더 자세히 살펴봅니다.
Helium의 fanout 프로그램에서 init 명령은 새 계정을 생성합니다. payer는 앞서 구조체에서 pub payer: Signer<'info>로 설정한 payer로 지정됩니다. 계정 공간은 FanoutVoucherV0의 크기에 판별자용 8바이트와 추가 공간 61바이트를 더한 값으로 설정됩니다.
...
#[account(
init,
payer = payer,
space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...init_if_needed
init_if_needed 제약 조건은 #[account(init_if_nedded, payer = <target_Account>)] 또는 #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]로 정의됩니다. 이 제약 조건은 init와 기능이 완전히 동일합니다. 하지만 계정이 아직 존재하지 않을 때만 실행됩니다. 계정이 존재하는 경우에도 init_if_needed는 올바른 공간이 할당되었는지, PDA인 경우 올바른 시드가 사용되었는지 등 모든 초기화 제약 조건이 충족되는지 확인합니다.
init_if_needed는 잠재적 위험 때문에 기능 플래그로 제한되므로 신중하게 사용해야 합니다. 활성화하려면 init-if-needed cargo 기능과 함께 anchor-lang를 가져오세요. init_if_needed를 사용할 때는 재초기화 공격을 방지하는 것이 중요합니다. 의도한 동작이 아니라면 초기화 후 계정이 초기 상태로 재설정되지 않도록 코드에 검사를 포함해야 합니다. 이러한 공격을 완화하려면 명령어 실행 경로를 단순하게 유지하는 것이 좋습니다. 초기화용 명령어 하나와 후속 작업용 명령어들로 분리하는 방식을 고려하세요.
Helium의 Fanout 프로그램은 init_if_needed 제약 조건을 사용해 recipient_account 계정이 아직 존재하지 않는 경우 이를 초기화합니다.
...
#[account(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...constraint
constraint 제약 조건은 #[account(constraint = <expr>)] 또는 #[account(constraint = <expr> @ <custom_error>)]로 정의됩니다. 제공된 표현식이 참으로 평가되는지 확인합니다. 의도한 사용 사례에 맞는 다른 제약 조건이 없을 때 유용합니다. @ 주석을 통한 사용자 지정 오류도 지원합니다.
Fanout 프로그램은 constraint를 사용해 민트의 공급량이 0으로 설정되어 있는지 확인합니다.
...
#[account(
mut,
constraint = mint.supply == 0,
mint::decimals = 0,
mint::authority = voucher,
mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...mint::authority, mint::decimals, mint::freeze_authority
위 코드 스니펫에서는 mint::decimals, mint::authority, mint::freeze_authority 제약 조건을 사용해 민트의 소수 자릿수가 0으로 설정되어 있는지, voucher에 권한과 동결 권한이 있는지 확인합니다.
참고로 mint::authority, mint::decimals, mint::freeze_authority 제약 조건은 다음과 같이 정의됩니다.
#[account(mint::authority = <target account>, mint::decimals = <expr>)]#[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]
이 제약 조건은 이름 그대로 각각 토큰의 권한, 소수 자릿수, 동결 권한을 확인합니다. 검사 용도로 사용하거나 init와 함께 사용해 지정된 민트 소수 자릿수와 민트 권한을 가진 민트 계정을 생성할 수 있습니다. init와 함께 사용할 때 동결 권한은 선택 사항입니다. 검사 용도로 사용할 때는 이 제약 조건의 일부만 지정할 수도 있습니다.
계정 공간
Solana에서 프로그램이 사용하는 모든 계정은 저장 공간을 명시적으로 할당해야 합니다. 이 할당은 효율적인 리소스 관리에 중요하며, 필요한 데이터만 온체인에 저장되도록 합니다. 또한 트랜잭션 비용을 예측 가능하게 하고 트랜잭션 실행 효율을 높입니다. 계정 저장 공간을 동적으로 할당하거나 크기를 조정하지 않고도 트랜잭션을 처리할 수 있습니다. 아울러 데이터를 미리 할당하면 계정에 필요한 모든 데이터를 저장할 공간이 확보되므로 트랜잭션 실패나 잠재적인 보안 취약점의 위험이 줄어듭니다.
변수 크기 산정
데이터 유형마다 필요한 공간이 다릅니다. 다음은 공간 요구 사항을 추정하는 데 도움이 되는 간단한 가이드입니다.
- 기본 유형: bool, u8, i8, u16, i16, u32, i32, u64, i64, u128, i128 같은 단순 데이터 유형은 모두 크기가 고정되어 있습니다. 1비트만 사용하지만
bool에는 1바이트가 필요하고,u128/i128에는 16바이트가 필요합니다 - 배열: 배열
[T;amount]의 공간은T의 크기에 요소 수를 곱해 계산합니다(즉,amount). 예를 들어u1616개로 이루어진 배열에는 32바이트가 필요합니다 - Pubkey: 공개 키는 Solana에서 항상 32바이트를 차지합니다
- 동적 유형:
String와Vec<T>는 신중히 고려해야 합니다. 둘 다 길이를 저장하는 데 4바이트가 필요하고, 실제 콘텐츠를 위한 공간도 필요합니다. 예상되는 최대 크기에 충분한 공간을 할당해야 합니다.String의 경우 4바이트에String의 바이트 길이를 더합니다.Vec<T>의 경우 4바이트에 해당 유형의 공간과 예상 요소 수를 곱한 값을 더합니다(즉, 4 + space(T) * amount) - 옵션과 열거형:
Option<T>유형에는 1바이트와T유형의 공간이 필요합니다. 열거형에는 열거형 판별자용 1바이트와 가장 큰 배리언트에 필요한 공간이 필요합니다 - 부동 소수점:
f32와f64같은 유형은 각각 4바이트와 8바이트를 차지합니다. NaN 값은 직렬화 실패를 일으킬 수 있으므로 주의하세요
다음 가이드는 zero-copy 직렬화를 사용하지 않는 계정에만 적용됩니다. 제로 카피 직렬화는 #[zero_copy] 속성으로 표시합니다. 메모리 레이아웃에 repr(c) 속성을 활용하므로 직접 포인터 캐스팅으로 데이터에 접근할 수 있습니다. 기존 역직렬화의 오버헤드 없이 온체인 데이터를 다루는 효율적인 방법입니다. #[zero_copy]는 #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)], #[repr(C)]를 적용하는 축약형입니다. 이러한 속성은 계정을 바이트 시퀀스로 안전하게 취급하고 제로 카피 역직렬화와 호환되도록 합니다. 제로 카피 역직렬화는 매우 큰 크기가 필요한 계정에 필수적입니다. 이러한 계정은 힙이나 스택 한도에 걸리지 않으면서 Borsh 또는 Anchor의 기본 직렬화 메커니즘으로 효율적으로 직렬화할 수 없습니다.
Anchor의 내부 판별자
개발자는 Anchor의 내부 판별자를 위해 space 제약 조건에 8을 더해야 합니다. 예를 들어 계정에 32바이트가 필요하다면 40바이트를 할당해야 합니다. 공간 계산에 내부 판별자가 반영되었음을 명확히 나타내려면 공간 제약 조건을 space = 8 + <account size>로 설정하는 것이 좋습니다.
참고로 판별자는 서로 다른 데이터 유형을 구분하는 고유 식별자입니다. 런타임에 서로 다른 계정 데이터 구조 유형을 구별할 때 유용합니다. 또한 명령 앞에 붙어 Anchor 프로그램 내의 해당 메서드로 명령을 라우팅하는 데 사용됩니다. 판별자는 데이터 유형의 고유 식별자를 나타내는 8바이트 배열입니다.
초기 공간 계산
계정의 초기 공간 요구 사항을 계산하기는 까다로울 수 있습니다. InitSpace 매크로는 계정 구조체에서 사용할 수 있는 INIT_SPACE 상수를 추가합니다. 상수를 생성하기 위해 구조체에 #[account] 매크로를 포함할 필요는 없습니다. Anchor 문서에서는 다음 예시를 제공합니다.
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
pub data: u64,
// max_len represents the length of the structure
#[max_len(50)]
pub string_one: String,
#[max_len(10, 5)]
pub nested: Vec<Vec<u8>>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
#[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
pub data: Account<'info, ExampleAccount>,
}이 예시에서 ExampleAccount::INIT_SPACE는 ExampleAccount에 필요한 공간을 자동으로 계산합니다. 공간을 계산할 때 Anchor의 내부 판별자도 반영합니다.
프로그램 공간 크기 조정
realloc 제약 조건은 명령 시작 시 프로그램 계정의 공간을 조정하는 데 사용됩니다. 계정이 변경 가능해야 하며(즉, mut), Account 또는 AccountLoader 유형에 적용할 수 있습니다. #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]로 정의됩니다. 계정 데이터 길이가 늘어나면 임대료 면제를 유지하기 위해 lamport가 realloc::payer에서 프로그램 계정으로 전송됩니다. 데이터 길이가 줄어들면 lamport가 프로그램 계정에서 realloc::payer로 돌아갑니다. realloc::zero 제약 조건은 새로 할당된 메모리를 0으로 초기화할지 결정합니다. 0으로 초기화하면 새 메모리가 깨끗하게 유지되고 잔존하거나 불필요한 데이터가 남지 않습니다.
realloc 제약 조건 대신 AccountInfo::realloc을 수동으로 사용하는 것은 권장하지 않습니다. 재할당이 MAX_PERMITTED_DATA_INCREASE 한도를 넘지 않도록 보장하는 런타임 검사가 없기 때문입니다. 이 경우 다른 계정의 데이터를 덮어쓸 수 있습니다. 이 제약 조건은 단일 명령 내에서 반복 재할당도 검사하고 방지합니다.
예를 들면 다음과 같습니다.
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [b"data"],
bump,
realloc = 8 + std::mem::size_of::<()>() + 48,
realloc::payer = payer,
realloc::zero = false
)]
pub update_account: Account<'info, NewData>,
system_program: Program<'info, System>,
}오류
오류 처리는 프로그램 개발의 핵심 요소입니다. 프로그램 실행을 중단시킬 수 있는 오류를 식별하고 관리하는 메커니즘입니다. 코드 품질, 유지보수성, 기능을 보장하려면 오류 처리를 의도적으로 계획해야 합니다. Anchor는 강력한 오류 처리 메커니즘으로 이 과정을 단순화합니다. Anchor 프로그램의 오류는 AnchorErrors와 비 Anchor 오류로 나눌 수 있습니다. 비 Anchor 오류에는 매우 다양한 Rust 오류가 포함되므로 이 섹션에서는 AnchorErrors에 중점을 둡니다. 비 Anchor 오류는 Rust Book의 오류 처리 장과 Rust By Example의 오류 처리 섹션을 참고하세요.
다음 struct는 AnchorError를 정의합니다.
pub struct AnchorError {
pub error_name: String,
pub error_code_number: u32,
pub error_msg: String,
pub error_origin: Option<ErrorOrigin>,
pub compared_values: Option<ComparedValues>,
}이 필드들은 비교적 이해하기 쉽습니다. error_name은 오류 이름을 나타내는 문자열입니다. error_code_number은 오류의 고유 식별자입니다. 즉, 32비트 공간을 차지하는 고유한 부호 없는 정수입니다. error_msg은 오류를 설명하는 메시지입니다. error_origin은 소스 파일이나 관련 계정 등 오류가 발생한 위치에 관한 정보를 제공하는 선택적 필드입니다. compared_values은 오류 발생 시 비교하던 값을 자세히 나타내는 선택적 필드입니다. 디버깅에 매우 유용합니다.
AnchorError은 로그 메서드를 구현합니다. 여기에는 오류의 발생 위치와 관련된 값에 관한 정보가 포함되어 디버깅과 오류 해결에 유용합니다. 이 메서드는 error_origin과 compared_values을 사용해 해당 정보를 제공합니다.
AnchorError은 Anchor 내부 오류와 사용자 정의 오류로 더 세분화할 수 있습니다. Anchor에는 반환될 수 있는 내부 오류 코드의 긴 목록이 있습니다. 이러한 내부 오류는 사용자가 직접 사용하도록 설계되지 않았습니다. 하지만 코드와 원인 간의 매핑을 알면 유용합니다. 일반적으로 제약 조건을 위반했을 때 발생합니다. 내부 오류 코드는 다음 스키마를 따릅니다.
- >= 100은 명령 오류 코드입니다
- >= 1000은 IDL 오류 코드입니다
- >= 2000은 제약 조건 오류 코드입니다
- >= 3000은 계정 오류 코드입니다
- >= 4100은 기타 오류 코드입니다
- = 5000은 더 이상 사용되지 않는 오류 코드입니다.
사용자 정의 오류는 ERROR_CODE_OFFSET, 즉 6000부터 시작합니다.
개발자는 error_code 속성으로 자체 사용자 정의 오류를 구현할 수 있습니다. 이 속성은 열거형에 사용하며, 열거형의 배리언트를 프로그램 전체에서 오류로 사용할 수 있습니다. 각 배리언트에 메시지를 추가할 수 있습니다. 오류가 발생하면 클라이언트가 이 메시지를 표시할 수 있습니다. 예를 들면 다음과 같습니다.
#[error_code]
pub enum HeliusError {
#[msg(“This RPC provider is too good”)]
RPCTooGood
}err! 및 error! 매크로를 사용해 이러한 오류를 발생시킬 수 있습니다. 예를 들면 다음과 같습니다.
require!(rpc.speed > 9000, HeliusError::RPCTooGood);선택할 수 있는 require 매크로가 여러 개라는 점에 유의해야 합니다. 이러한 매크로는 대부분 공개 키가 아닌 값을 다룹니다. 예를 들어 require_gte 매크로는 공개 키가 아닌 첫 번째 값이 공개 키가 아닌 두 번째 값보다 크거나 같은지 확인합니다.
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}공개 키를 비교할 때도 몇 가지 주의 사항이 있습니다. 예를 들어 개발자는 require_eq보다 require_keys_eq을 사용해야 합니다. 후자가 비용이 더 많이 들기 때문입니다.
모든 프로그램은 ProgramError를 반환합니다. 이 오류 유형에는 사용자 정의 오류 번호를 위한 전용 필드가 있으며, Anchor는 여기에 내부 오류 코드와 사용자 정의 오류 코드를 저장합니다. 하지만 단일 숫자에 불과하므로 유용성이 떨어집니다. 앞서 설명한 Anchor의 AnchorError 로깅이 훨씬 유용합니다. Anchor 클라이언트는 이러한 로그를 파싱하도록 설계되었습니다. 하지만 이 과정이 어려운 경우도 있습니다. 예를 들어 사전 실행 검사를 끈 상태에서 처리된 트랜잭션의 로그를 가져오는 작업은 간단하지 않습니다. 마찬가지로 Anchor는 AnchorError을 표준 방식으로 기록하지 않는 비 Anchor 또는 레거시 프로그램을 위한 폴백 메커니즘도 사용합니다. 이 경우 Anchor는 트랜잭션이 반환한 오류 번호가 Anchor 내부 오류 코드 또는 프로그램의 IDL에 정의된 오류 번호와 일치하는지 확인합니다. 일치 항목이 발견되면 Anchor는 더 많은 맥락을 제공하도록 오류 정보를 보강합니다. 또한 가능하면 프로그램 오류 스택을 파싱해 프로그램 오류의 최초 원인을 추적합니다. ProgramError은 기본 오류 유형이며, Anchor의 로깅 및 파싱 메커니즘이 상세한 오류 정보를 제공해 그 활용도를 높입니다.
크로스 프로그램 호출(CPI)
이 글 전반에서 크로스 프로그램 호출(CPI)을 언급했으므로 별도 섹션에서 다루는 것이 적절합니다. CPI를 사용하면 프로그램이 다른 프로그램을 직접 호출할 수 있으므로 Solana의 조합 가능성에 핵심적인 역할을 합니다. 이를 통해 Solana 생태계 전체가 개발자를 위한 방대하고 상호 연결된 API처럼 작동합니다. 간결한 설명을 위해 인형과 인형 조종자 프로그램으로 CPI의 실제 동작을 보여주는 Anchor의 CPI 문서를 읽어보시기를 권합니다.
CPI는 한 프로그램이 호출 대상 프로그램의 특정 명령을 지정해 다른 프로그램을 호출하는 것으로 정의할 수 있습니다. 호출한 프로그램은 호출된 프로그램이 명령 처리를 마칠 때까지 중단됩니다.
권한 상승
CPI를 사용하면 호출 프로그램이 서명자 권한을 피호출 프로그램으로 확장할 수 있습니다. 권한 확장은 편리하지만 매우 위험할 수 있습니다. CPI가 실수로 악성 프로그램을 대상으로 하면 해당 프로그램은 호출자와 동일한 권한을 얻습니다. Anchor는 두 가지 보호 장치로 이 위험을 줄입니다.
- Program<’info, T> 유형은 지정된 계정이 예상 프로그램(T)과 일치하는지 확인합니다
- Program 유형을 사용하지 않더라도 자동 생성된 CPI 함수가 cpi_program 인수가 예상 프로그램과 일치하는지 확인합니다
CPI 실행
프로그램은 solana_program 크레이트의 invoke 또는 invoke_signed를 사용해 CPI를 실행할 수 있습니다. Anchor는 CPI의 인수가 아닌 입력을 지정할 수 있는 CpiContext 구조체도 제공합니다.
invoke
invoke 함수는 PDA가 서명으로 필요하지 않을 때 사용합니다. 이 경우 런타임은 호출 프로그램의 원래 서명을 피호출 프로그램으로 확장합니다. 함수는 다음과 같이 정의됩니다.
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> ProgramResult다른 프로그램을 호출하려면 프로그램 ID, 피호출 프로그램용 명령 데이터, 피호출 프로그램이 접근할 계정 목록을 포함하는 Instruction을 생성해야 합니다. 프로그램은 프로그램 진입점에서 런타임으로부터 AccountInfo 값만 받습니다. 피호출 프로그램이 호출에 필요로 하는 모든 계정은 호출하는 프로그램이 포함하고 제공해야 합니다. 예를 들어 피호출 프로그램이 특정 계정을 수정해야 한다면 호출자 프로그램은 AccountInfo 값 목록에 해당 계정을 포함해야 합니다. 피호출 프로그램의 프로그램 ID에도 동일하게 적용됩니다. 즉, 호출자는 피호출 프로그램의 프로그램 ID를 포함해 어떤 프로그램을 호출할지 명시적으로 지정해야 합니다.
Instruction은 일반적으로 호출 프로그램 내에서 구성하지만 외부 출력에서 역직렬화할 수도 있습니다.
피호출 프로그램에서 오류가 발생하거나 실행이 중단되면 전체 트랜잭션이 즉시 실패합니다. invoke 함수는 성공한 경우에만 반환되기 때문입니다. CPI 결과로 데이터를 반환하려면 set_return_data 또는 get_return_data 함수를 사용하세요. 반환되는 유형은 AnchorSerialize 및 AnchorDeserialize 트레이트를 구현해야 합니다. 또는 피호출 프로그램이 데이터 저장 전용 계정에 쓰도록 하세요
프로그램은 자신을 재귀적으로 호출할 수 있지만 다른 프로그램에 의한 간접 재귀 호출, 즉 재진입은 트랜잭션을 즉시 실패시킵니다.
예를 들어 CPI를 통해 토큰을 전송하는 프로그램이 있다면 invoke을 다음과 같이 사용합니다.
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}invoke_signed
invoke_signed은 PDA가 서명자로 필요한 CPI에 사용합니다. 호출 프로그램이 PDA를 파생하는 데 필요한 시드를 제공해 해당 PDA를 대신해 동작할 수 있습니다.
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> ProgramResultPDA도 CPI에서 서명자 역할을 할 수 있습니다. 런타임은 제공된 시드와 호출 프로그램의 program_id을 사용하고 create_program_address을 통해 내부적으로 PDA를 생성합니다. 그런 다음 PDA를 명령과 함께 전달된 주소(즉, account_infos)와 대조해 유효한 서명자인지 확인합니다.
이 함수를 사용하면 호출 프로그램이 제어하는 하나 이상의 PDA를 대신해 호출에 서명할 수 있습니다. 따라서 피호출 프로그램은 해당 계정이 암호학적으로 서명된 것처럼 상호작용할 수 있습니다. signer_seeds은 PDA를 파생하는 데 사용되는 시드 슬라이스로 구성됩니다. 호출 중 런타임은 account_info에서 일치하는 모든 계정을 “서명됨”으로 간주합니다. 예를 들어 PDA용 계정을 생성하는 프로그램이 있다면 invoke_signed을 다음과 같이 호출합니다.
invoke_signed(
&system_instruction::create_account(
&payer.key,
&vault_pda.key,
lamports,
vault_size,
&program_id,
),
&[
payer.clone(),
vault_pda.clone(),
],
&[
&[
b"vault",
payer.key.as_ref(),
&[vault_bump_seed],
],
]
)?;CpiContext
Anchor는 invoke 또는 invoke_signed 대신 CPI를 더 간단히 수행할 수 있도록 CpiContext를 제공합니다. 이 구조체는 CPI에 필요한 인수가 아닌 입력을 지정하며, Context의 기능과 매우 유사합니다. 명령에 필요한 계정, 관련된 추가 계정, 호출할 프로그램 ID, 필요한 경우 PDA 파생용 시드에 관한 정보를 제공합니다. PDA가 없는 CPI에는 CpiContext::new을 사용하고, PDA 서명자가 필요한 CPI에는 CpiContext::new_with_signer을 사용하세요.
CpiContext은 다음과 같이 정의됩니다. 여기서 T은 ToAccountMetas 및 ToAccountInfos<’info> 트레이트를 구현하는 모든 객체를 포괄하는 제네릭 유형입니다.
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
T: ToAccountMetas + ToAccountInfos<'info>,{
pub accounts: T,
pub remaining_accounts: Vec>,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}Accounts은 제네릭 유형이므로 ToAccountMetas 및 ToAccountInfos<’info> 트레이트를 구현하는 모든 객체를 사용할 수 있습니다. #[derive(Accounts)] 속성 매크로가 이를 지원해 코드를 체계화하고 유형 안전성을 높입니다.
CpiContext은 Anchor 및 비 Anchor 프로그램 호출을 간소화합니다. Anchor 프로그램의 경우 프로젝트의 Cargo.toml 파일에 종속성을 선언하고 Anchor가 생성한 cpi 모듈을 사용하면 됩니다.
[dependencies]
callee = { path = "../callee", features = ["cpi"]}features = [“cpi”]을 설정하면 프로그램이 callee::cpi 모듈에 접근할 수 있습니다. Anchor는 이 모듈을 자동으로 생성하고 프로그램의 명령을 Rust 함수로 노출합니다. 이 함수는 CpiContext과 추가 명령 데이터를 받습니다. Context 대신 CpiContext을 사용한다는 점을 제외하면 Anchor 프로그램의 일반 명령 함수 형식과 같습니다. cpi 모듈은 명령 호출에 필요한 계정 구조체도 제공합니다.
예를 들어 피호출 프로그램에 GeneralKenobi 구조체로 정의된 특정 계정이 필요한 hello_there 명령이 있다면 다음과 같이 호출합니다.
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;
#[program]
pub mod fight_on_utapau {
use super::*;
pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
let cpi_accounts = GeneralKenobi {
jedi: ctx.accounts.jedi.to_account_info(),
// Other account infos needed for the GeneralKenobi struct go here
};
let cpi_program = ctx.accounts.jedi_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
hello_there(cpi_ctx, data);
}
#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
pub jedi: UncheckedAccount<'info>,
pub jedi_program: Program<'info, Jedi>,
// Other required accounts
}
pub struct GreetingParams {
// Params required for the hello_there function
}fight_on_utapau 모듈에서는 CpiContext을 사용해 CPI를 수행합니다. call_hello_there 함수는 jedi 프로그램과 상호작용하도록 설계되었습니다. jedi 프로그램의 GeneralKenobi 계정 구조체에 필요한 계정 정보와 jedi 프로그램의 계정 정보를 사용해 CpiContext을 생성합니다. 이 컨텍스트는 hello_there을 호출하면서 GreetingParams 구조체에 지정된 추가 필수 매개변수를 전달합니다. CallGeneralKenobi 구조체는 이 함수에 필요한 계정을 정의해 프로세스를 간소화합니다.
마지막으로 비 Anchor 프로그램의 명령을 호출할 때는 프로그램 관리자가 프로그램 호출용 헬퍼 함수가 포함된 자체 크레이트를 게시했는지 확인하세요. 호출해야 할 명령이 있는 프로그램에 헬퍼 함수가 없다면 invoke 및 invoke_signer을 사용해 CPI를 구성하고 준비하세요.
프로그램 파생 주소(PDA)
PDA는 곡선 밖에 있으며 연결된 비공개 키가 없다는 점을 기억하세요. PDA를 사용하면 프로그램이 명령에 서명할 수 있고 개발자는 온체인에 해시맵과 유사한 구조를 구축할 수 있습니다. PDA는 선택적 시드 목록, 범프 시드, 프로그램 ID를 사용해 파생합니다.
다시 정리하면 다음 제약 조건을 사용해 주어진 계정이 현재 실행 중인 프로그램, 시드, 그리고 제공된 경우 범프에서 파생된 PDA인지 확인합니다.
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
범프가 제공되지 않으면 Anchor는 정규 범프를 사용합니다. Seeds::program = <expr>을 사용하면 현재 실행 중인 프로그램이 아닌 다른 프로그램에서 PDA를 파생할 수 있습니다.
seeds 및 bump 제약 조건을 사용하면 파생 과정이 간소화됩니다.
#[derive(Accounts)]
struct ExamplePDA<'info> {
#[account(seeds = [b"example"], bump)]
pub example_pda: Account<'info, AccountType>,
}여기서는 seeds 제약 조건을 사용해 PDA를 파생합니다. Anchor는 명령에 전달된 계정이 시드에서 파생된 PDA와 일치하는지 자동으로 확인합니다. 특정 값 없이 범프 제약 조건을 사용하면 Anchor는 기본적으로 정규 범프를 사용합니다.
Anchor는 다른 계정 필드나 명령 데이터를 기반으로 한 동적 시드도 지원합니다. 구조체 내의 다른 필드를 참조하거나 #[instruction(...)] 속성 매크로를 사용해 역직렬화된 명령 데이터를 포함하면 됩니다. 예를 들어 다음 구조체에서 example_pda은 정적 시드, 명령 데이터, 서명자의 공개 키 조합을 사용하도록 제한됩니다.
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
pub example_pda: Account<'info, AccountType>,
#[account(mut)]
pub signoooorrr: Signer<'info>
}결론
Anchor를 단순히 강력한 프레임워크라고만 표현하기에는 부족합니다. 다양한 매크로와 트레이트를 활용해 코드를 줄이는 방식을 살펴보면, 개발 프로세스를 얼마나 효과적으로 간소화하는지 알 수 있습니다. 잘 관리된 문서와 관련 튜토리얼 및 크레이트로 구성된 탄탄한 생태계도 뒷받침합니다. 대다수 Solana 개발자가 Anchor를 선호하며 사용하고 있습니다.
이 글은 Anchor 프로그램 개발을 다루는 매우, 매우 포괄적인 가이드입니다. Anchor 설치와 Solana Playground 사용법부터 Hello, World! 프로그램의 생성, 빌드, 배포까지 설명했습니다. 이어서 Anchor의 효과적인 추상화 방식, 일반적인 Anchor 프로그램의 구조, 사용할 수 있는 다양한 계정 유형과 제약 조건을 살펴봤습니다. 계정 공간 할당과 오류 처리의 중요성도 다뤘습니다. 마지막으로 CPI와 PDA를 살펴봤습니다. 이 글은 Anchor를 위한 결정판입니다. 지금 바로 Solana 프로그램 개발을 시작하는 데 필요한 모든 내용을 담았습니다.
여기까지 읽어주셔서 감사합니다, 익명의 독자님! 아래에 이메일 주소를 입력하고 Solana의 새로운 소식을 빠짐없이 받아보세요. 더 깊이 알아볼 준비가 되셨나요? Anchor 프로그램 개발을 시작하려면 Discord에 참여하세요.
추가 자료
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


