Solana 계정 모델
Solana에서 모든 데이터는 "계정"이라고 불리는 곳에 저장됩니다. Solana의 데이터는 단일 "계정" 테이블이 있는 공개 데이터베이스로 생각할 수 있으며, 이 테이블의 각 항목이 "계정"입니다. 모든 Solana 계정은 동일한 기본 계정 유형을 공유합니다.
계정
주요 포인트
- 계정은 최대 10MiB의 데이터를 저장할 수 있으며, 실행 가능한 프로그램 코드나 프로그램 상태를 포함합니다.
- 계정은 저장된 데이터의 양에 비례하는 lamport(SOL) 단위의 rent 예치금이 필요하며, 계정을 닫을 때 이를 완전히 회수할 수 있습니다.
- 모든 계정에는 프로그램 소유자가 있습니다. 계정을 소유한 프로그램만 해당 데이터를 변경하거나 lamport 잔액을 차감할 수 있습니다. 하지만 누구나 잔액을 증가시킬 수 있습니다.
- Sysvar 계정은 네트워크 클러스터 상태를 저장하는 특별한 계정입니다.
- 프로그램 계정은 스마트 컨트랙트의 실행 가능한 코드를 저장합니다.
- 데이터 계정은 프로그램이 프로그램 상태를 저장하고 관리하기 위해 생성합니다.
계정
Solana의 모든 계정은 고유한 32바이트 주소를 가지며, 주로 base58로 인코딩된
문자열(예: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo6X8TsVXREG
)로 표시됩니다.
계정과 그 주소 간의 관계는 키-값 쌍처럼 작동하며, 주소는 계정의 온체인 데이터를 찾기 위한 키 역할을 합니다. 계정 주소는 "계정" 테이블의 각 항목에 대한 "고유 ID" 역할을 합니다.
계정 주소
대부분의 Solana 계정은 주소로 Ed25519 공개 키를 사용합니다.
import { generateKeyPairSigner } from "@solana/kit";// Kit does not enable extractable private keysconst keypairSigner = await generateKeyPairSigner();console.log(keypairSigner);
공개 키가 일반적으로 계정 주소로 사용되지만, Solana는 프로그램 파생 주소(Program Derived Addresses, PDAs)라는 기능도 지원합니다. PDA는 프로그램 ID와 선택적 입력(seed)에서 결정적으로 파생할 수 있는 특별한 주소입니다.
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
계정 유형
계정의 최대 크기는 10MiB이며, Solana의 모든 계정은 동일한 기본 Account 유형을 공유합니다.
계정 유형
Solana의 모든 계정은 다음 필드를 가지고 있습니다:
data
: 계정의 임의 데이터를 저장하는 바이트 배열입니다. 실행 불가능한 계정의 경우, 이는 주로 읽기 위한 상태를 저장합니다. 프로그램 계정(스마트 컨트랙트)의 경우, 실행 가능한 프로그램 코드를 포함합니다. 이 데이터 필드는 일반적으로 "계정 데이터"라고 불립니다.executable
: 이 플래그는 계정이 프로그램인지 여부를 나타냅니다.lamports
: lamport로 표시된 계정의 잔액입니다. lamport는 SOL의 가장 작은 단위입니다(1 SOL = 10억 lamport).owner
: 이 계정을 소유한 프로그램의 프로그램 ID(공개 키)입니다. 소유자 프로그램만이 계정의 데이터를 변경하거나 lamport 잔액을 차감할 수 있습니다.rent_epoch
: Solana가 계정에서 주기적으로 lamport를 차감하는 메커니즘을 가졌을 때의 레거시 필드입니다. 이 필드는 Account 유형에 여전히 존재하지만, rent 수집이 더 이상 사용되지 않기 때문에 현재는 사용되지 않습니다.
pub struct Account {/// lamports in the accountpub lamports: u64,/// data held in this account#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]pub data: Vec<u8>,/// the program that owns this account. If executable, the program that loads this account.pub owner: Pubkey,/// this account's data contains a loaded program (and is now read-only)pub executable: bool,/// the epoch at which this account will next owe rentpub rent_epoch: Epoch,}
import {airdropFactory,createSolanaRpc,createSolanaRpcSubscriptions,generateKeyPairSigner,lamports} from "@solana/kit";// Create a connection to Solana clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate a new keypairconst keypair = await generateKeyPairSigner();console.log(`Public Key: ${keypair.address}`);// Funding an address with SOL automatically creates an accountconst signature = await airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: keypair.address,lamports: lamports(1_000_000_000n),commitment: "confirmed"});const accountInfo = await rpc.getAccountInfo(keypair.address).send();console.log(accountInfo);
Rent
온체인에 데이터를 저장하기 위해서는 계정이 저장된 데이터의 양(바이트)에 비례하는 lamport(SOL) 잔액을 유지해야 합니다. 이 잔액을 "rent"라고 하지만, 계정을 닫을 때 전체 금액을 회수할 수 있기 때문에 보증금처럼 작동합니다. 계산 방법은 이러한 상수를 사용하여 여기에서 확인할 수 있습니다.
"rent"라는 용어는 rent 임계값 미만인 계정에서 정기적으로 lamport를 공제하던 더 이상 사용되지 않는 메커니즘에서 유래되었습니다. 이 메커니즘은 더 이상 활성화되어 있지 않습니다.
프로그램 소유자
Solana에서 "스마트 컨트랙트"는 프로그램이라고 불립니다. 프로그램 소유권은 Solana 계정 모델의 핵심 부분입니다. 모든 계정에는 소유자로 지정된 프로그램이 있습니다. 소유자 프로그램만이 다음을 수행할 수 있습니다:
- 계정의
data
필드 변경 - 계정 잔액에서 lamport 공제
시스템 프로그램
기본적으로 모든 새 계정은 시스템 프로그램이 소유합니다. 시스템 프로그램은 몇 가지 주요 기능을 수행합니다:
- 새 계정 생성: 시스템 프로그램만이 새 계정을 생성할 수 있습니다.
- 공간 할당: 각 계정의 데이터 필드에 대한 바이트 용량을 설정합니다.
- 프로그램 소유권 이전/할당: 시스템 프로그램이 계정을 생성하면, 지정된 프로그램 소유자를 다른 프로그램 계정으로 재할당할 수 있습니다. 이것이 커스텀 프로그램이 시스템 프로그램에 의해 생성된 새 계정의 소유권을 가져가는 방법입니다.
Solana의 모든 "지갑" 계정은 시스템 프로그램이 소유한 계정일 뿐입니다. 이러한 계정의 lamport 잔액은 지갑이 소유한 SOL의 양을 보여줍니다. 시스템 프로그램이 소유한 계정만이 트랜잭션 수수료를 지불할 수 있습니다.
시스템 계정
Sysvar 계정
Sysvar 계정은 미리 정의된 주소에 있는 특별한 계정으로, 클러스터 상태 데이터에 접근할 수 있게 해줍니다. 이 계정들은 네트워크 클러스터에 관한 데이터로 동적으로 업데이트됩니다. Sysvar 계정의 전체 목록은 여기에서 확인할 수 있습니다.
import { Address, createSolanaRpc } from "@solana/kit";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const SYSVAR_CLOCK_ADDRESS ="SysvarC1ock11111111111111111111111111111111" as Address;const accountInfo = await rpc.getAccountInfo(SYSVAR_CLOCK_ADDRESS, { encoding: "base64" }).send();console.log(accountInfo);
프로그램 계정
Solana 프로그램을 배포하면 실행 가능한 프로그램 계정이 생성됩니다. 프로그램 계정은 프로그램의 실행 가능한 코드를 저장합니다.
프로그램 계정은 로더 프로그램이 소유합니다.
프로그램 계정
간단히 말해, 프로그램 계정을 프로그램 자체로 취급할 수 있습니다. 프로그램의 명령을 호출할 때, 프로그램 계정의 주소(일반적으로 "Program ID"라고 불림)를 지정합니다.
import { Address, createSolanaRpc } from "@solana/kit";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const programId = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;const accountInfo = await rpc.getAccountInfo(programId, { encoding: "base64" }).send();console.log(accountInfo);
Solana 프로그램을 배포하면 프로그램 계정에 저장됩니다. 프로그램 계정은 로더 프로그램이 소유합니다. 로더에는 여러 버전이 있지만, loader-v3를 제외한 모든 버전은 실행 가능한 코드를 프로그램 계정에 직접 저장합니다. Loader-v3는 실행 가능한 코드를 별도의 "프로그램 데이터 계정"에 저장하고 프로그램 계정은 단지 이를 가리킵니다. 새 프로그램을 배포할 때, Solana CLI는 기본적으로 최신 로더 버전을 사용합니다.
버퍼 계정
Loader-v3에는 배포 또는 재배포/업그레이드 중에 프로그램 업로드를 임시로 준비하기 위한 특별한 계정 유형이 있습니다. Loader-v4에서도 버퍼가 있지만, 이들은 단지 일반 프로그램 계정입니다.
프로그램 데이터 계정
Loader-v3는 다른 모든 BPF 로더 프로그램과 다르게 작동합니다. 프로그램 계정은 실제 실행 가능한 코드를 저장하는 프로그램 데이터 계정의 주소만 포함합니다:
프로그램 데이터 계정
이러한 프로그램 데이터 계정을 프로그램의 데이터 계정(아래 참조)과 혼동하지 마세요.
데이터 계정
Solana에서는 프로그램의 실행 가능한 코드가 프로그램의 상태와는 다른 계정에 저장됩니다. 이는 운영 체제가 일반적으로 프로그램과 해당 데이터를 별도의 파일로 관리하는 방식과 유사합니다.
상태를 유지하기 위해 프로그램은 자신이 소유한 별도의 계정을 생성하는 명령어를 정의합니다. 이러한 각 계정은 고유한 주소를 가지며 프로그램에 의해 정의된 임의의 데이터를 저장할 수 있습니다.
데이터 계정
시스템 프로그램만이 새 계정을 생성할 수 있다는 점에 유의하세요. 시스템 프로그램이 계정을 생성하면 새 계정의 소유권을 다른 프로그램으로 이전하거나 할당할 수 있습니다.
즉, 커스텀 프로그램을 위한 데이터 계정을 생성하는 과정은 두 단계로 이루어집니다:
- 시스템 프로그램을 호출하여 계정을 생성한 다음, 소유권을 커스텀 프로그램으로 이전합니다
- 이제 계정을 소유한 커스텀 프로그램을 호출하여 프로그램의 명령어에 정의된 대로 계정 데이터를 초기화합니다
이 계정 생성 과정은 종종 단일 단계로 추상화되지만, 기본 프로세스를 이해하는 것이 도움이 됩니다.
import {airdropFactory,appendTransactionMessageInstructions,createSolanaRpc,createSolanaRpcSubscriptions,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners} from "@solana/kit";import { getCreateAccountInstruction } from "@solana-program/system";import {getInitializeMintInstruction,getMintSize,TOKEN_2022_PROGRAM_ADDRESS} from "@solana-program/token-2022";// Create Connection, local validator in this exampleconst rpc = createSolanaRpc("http://127.0.0.1:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate keypairs for fee payerconst feePayer = await generateKeyPairSigner();// Fund fee payerawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: feePayer.address,lamports: lamports(1_000_000_000n),commitment: "confirmed"});// Generate keypair to use as address of mintconst mint = await generateKeyPairSigner();// Get default mint account size (in bytes), no extensions enabledconst space = BigInt(getMintSize());// Get minimum balance for rent exemptionconst rent = await rpc.getMinimumBalanceForRentExemption(space).send();// Instruction to create new account for mint (token 2022 program)// Invokes the system programconst createAccountInstruction = getCreateAccountInstruction({payer: feePayer,newAccount: mint,lamports: rent,space,programAddress: TOKEN_2022_PROGRAM_ADDRESS});// Instruction to initialize mint account data// Invokes the token 2022 programconst initializeMintInstruction = getInitializeMintInstruction({mint: mint.address,decimals: 9,mintAuthority: feePayer.address});const instructions = [createAccountInstruction, initializeMintInstruction];// Get latest blockhash to include in transactionconst { value: latestBlockhash } = await rpc.getLatestBlockhash().send();// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }), // Create transaction message(tx) => setTransactionMessageFeePayerSigner(feePayer, tx), // Set fee payer(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set transaction blockhash(tx) => appendTransactionMessageInstructions(instructions, tx) // Append instructions);// Sign transaction message with required signers (fee payer and mint keypair)const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Send and confirm transactionawait sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });// Get transaction signatureconst transactionSignature = getSignatureFromTransaction(signedTransaction);console.log("Mint Address:", mint.address);console.log("Transaction Signature:", transactionSignature);const accountInfo = await rpc.getAccountInfo(mint.address).send();console.log(accountInfo);
Is this page helpful?