Solanaアカウントモデル
Solanaでは、すべてのデータは「アカウント」と呼ばれるものに格納されます。Solana上のデータは、単一の「アカウント」テーブルを持つ公開データベースと考えることができ、このテーブルの各エントリが「アカウント」です。すべてのSolanaアカウントは同じ基本的なアカウントタイプを共有しています。
アカウント
重要ポイント
- アカウントは最大10MiBのデータを格納でき、実行可能なプログラムコードまたはプログラム状態が含まれます。
- アカウントは格納されるデータ量に比例したlamport(SOL)でのrentデポジットが必要で、アカウントを閉じると完全に回収できます。
- すべてのアカウントにはプログラム所有者があります。アカウントを所有するプログラムだけがそのデータを変更したりlamport残高を減らしたりできます。ただし、誰でも残高を増やすことはできます。
- Sysvarアカウントはネットワーククラスターの状態を格納する特別なアカウントです。
- program accountはスマートコントラクトの実行可能コードを格納します。
- データアカウントはプログラムによって作成され、プログラム状態を格納・管理します。
アカウント
Solana上のすべてのアカウントは、32バイトの一意のアドレスを持ち、多くの場合base58エンコードされた文字列として表示されます(例:14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5
)。
アカウントとそのアドレスの関係は、キーバリューペアのように機能し、アドレスはアカウントのオンチェーンデータを特定するためのキーとなります。アカウントアドレスは「アカウント」テーブルの各エントリの「一意の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(PDA)と呼ばれる機能もサポートしています。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上のすべてのアカウントには以下のフィールドがあります。
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,}
Lamportsフィールド
アカウントの残高はlamport単位で表示されます。lamportはSOLの最小単位です(1 SOL =
10億lamport)。アカウントのSOL残高は、lamports
フィールドの金額をSOLに変換したものです。
Solanaアカウントは、アカウントに保存されているデータ量(バイト単位)に比例した最小限のlamport残高を持つ必要があります。この最小残高は「rent」と呼ばれます。
アカウントに保存されているlamport残高は、アカウントが閉鎖されると完全に回収できます。
データフィールド
アカウントの任意のデータを格納するバイト配列です。データフィールドは一般的に「アカウントデータ」と呼ばれます。
- program account(スマートコントラクト)の場合、このフィールドには実行可能なプログラムコード自体、または実行可能なプログラムコードを格納する別のアカウントのアドレスが含まれます。
- 実行不可能なアカウントの場合、通常は読み取り対象の状態が格納されます。
Solanaアカウントからデータを読み取るには、次の2つのステップが必要です:
- アドレス(公開鍵)を使用してアカウントを取得する
- アカウントのデータフィールドを生のバイトから適切なデータ構造に逆シリアル化する(この構造はアカウントを所有するプログラムによって定義されます)
オーナーフィールド
このアカウントを所有するプログラムのプログラムID(公開鍵)です。
すべてのSolanaアカウントには、所有者として指定されたプログラムがあります。アカウントを所有するプログラムだけが、そのアカウントのデータを変更したり、lamport残高を差し引いたりできます。
プログラムで定義された instructions は、アカウントのデータと lamport 残高をどのように変更できるかを決定します。
実行可能フィールド
このフィールドは、アカウントが実行可能なプログラムであるかどうかを示します。
true
の場合、アカウントは実行可能なSolanaプログラムです。false
の場合、アカウントは状態を保存するデータアカウントです。
実行可能なアカウントでは、owner
フィールドにはローダープログラムのプログラムIDが含まれています。ローダープログラムは、実行可能なプログラムアカウントを読み込み、管理する役割を持つ組み込みプログラムです。
Rent Epochフィールド
rent_epoch
フィールドは、現在は使用されていないレガシーフィールドです。
元々、このフィールドはアカウントがネットワーク上でデータを維持するために rent(lamportで)を支払う必要がある時期を追跡していました。しかし、このrent徴収メカニズムはその後廃止されました。
Rent
オンチェーンでデータを保存するには、アカウントは保存されるデータ量(バイト単位)に比例したlamport(SOL)残高も維持する必要があります。この残高は「rent」と呼ばれますが、アカウントを閉じると全額回収できるため、デポジットのような仕組みになっています。計算方法はこちらで、これらの定数を使用しています。
「rent」という用語は、rent閾値を下回るアカウントから定期的にlamportを差し引く廃止されたメカニズムに由来しています。このメカニズムは現在は有効ではありません。
プログラムオーナー
Solanaでは、「スマートコントラクト」はプログラムと呼ばれています。プログラムの所有権はSolanaアカウントモデルの重要な部分です。すべてのアカウントには、オーナーとして指定されたプログラムがあります。オーナープログラムのみが次のことを行えます:
- アカウントの
data
フィールドを変更する - アカウントの残高からlamportを差し引く
各プログラムはアカウントのdata
フィールドに保存されるデータの構造を定義します。プログラムのinstructionsによって、このデータとアカウントの
lamports
残高がどのように変更されるかが決まります。
System Program
デフォルトでは、すべての新しいアカウントは System Programに所有されています。System Programは以下の主要な機能を実行します:
機能 | 説明 |
---|---|
新規アカウント作成 | System Programのみが新しいアカウントを作成できます。 |
スペース割り当て | 各アカウントのデータフィールドのバイト容量を設定します。 |
プログラム所有権の割り当て | System Programがアカウントを作成した後、指定されたプログラム所有者を別のprogram accountに再割り当てすることができます。これにより、カスタムプログラムがSystem Programによって作成された新しいアカウントの所有権を取得します。 |
SOL転送 | System Accountから他のアカウントへlamport(SOL)を転送します。 |
Solana上のすべての「ウォレット」アカウントはSystem Programが所有する「System Account」であることに注意してください。これらのアカウントのlamport残高は、ウォレットが所有するSOLの量を示しています。System Accountのみがトランザクション手数料を支払うことができます。
System Account
SOLが初めて新しいアドレスに送信されると、そのアドレスにSystem Programが所有するアカウントが自動的に作成されます。
以下の例では、新しいkeypairが生成され、SOLで資金提供されます。コードを実行して出力を確認してください。アカウントのowner
フィールドがアドレス11111111111111111111111111111111
のSystem
Programであることに注意してください。
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);
Sysvarアカウント
Sysvarアカウントは、クラスターの状態データへのアクセスを提供する、事前に定義されたアドレスにある特別なアカウントです。これらのアカウントは、ネットワーククラスターに関するデータで動的に更新されます。Sysvarアカウントの完全なリストはこちらで確認できます。
次の例は、Sysvar Clockアカウントからデータを取得して逆シリアル化する方法を示しています。
import { createSolanaRpc } from "@solana/kit";import { fetchSysvarClock, SYSVAR_CLOCK_ADDRESS } from "@solana/sysvars";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const accountInfo = await rpc.getAccountInfo(SYSVAR_CLOCK_ADDRESS, { encoding: "base64" }).send();console.log(accountInfo);// Automatically fetch and deserialize the account dataconst clock = await fetchSysvarClock(rpc);console.log(clock);
program account
Solanaプログラムをデプロイすると、実行可能なprogram accountが作成されます。program accountはプログラムの実行可能コードを保存します。program accountはローダープログラムによって所有されています。
Program Account
簡単に言えば、program accountをプログラム自体として扱うことができます。プログラムのinstructionsを呼び出す際、program accountのアドレス(一般的に「プログラムID」と呼ばれる)を指定します。
次の例では、Token Programアカウントを取得して、program accountが同じ基本
Account
型を持っていることを示しています。ただし、executable
フィールドは
true
に設定されています。program
accountはデータフィールドに実行可能コードを含むため、データを逆シリアル化しません。
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プログラムをデプロイすると、それはprogram accountに保存されます。program accountはローダープログラムによって所有されています。ローダーにはいくつかのバージョンがありますが、loader-v3を除くすべてのバージョンは実行可能コードを直接program accountに保存します。loader-v3は実行可能コードを別の「プログラムデータアカウント」に保存し、program accountはそれを指すだけです。新しいプログラムをデプロイする場合、Solana CLIはデフォルトで最新のローダーバージョンを使用します。
バッファアカウント
Loader-v3には、デプロイメントやアップグレード中にプログラムのアップロードを一時的にステージングするための特別なアカウントタイプがあります。loader-v4でもバッファは存在しますが、それらは単なる通常のプログラムアカウントです。
プログラムデータアカウント
Loader-v3は他のすべてのBPF Loaderプログラムとは異なる動作をします。プログラムアカウントには実行可能コードを格納するプログラムデータアカウントのアドレスのみが含まれています:
プログラムデータアカウント
これらのプログラムデータアカウントをプログラムのデータアカウント(以下参照)と混同しないでください。
データアカウント
Solanaでは、プログラムの実行可能コードはプログラムの状態とは異なるアカウントに格納されます。これは、オペレーティングシステムが通常プログラムとそのデータを別々のファイルに持つのと似ています。
状態を維持するために、プログラムは所有する別個のアカウントを作成するための指示を定義します。これらの各アカウントには独自のユニークなアドレスがあり、プログラムによって定義された任意のデータを格納できます。
データアカウント
新しいアカウントを作成できるのはSystem Programだけであることに注意してください。System Programがアカウントを作成すると、その後、新しいアカウントの所有権を別のプログラムに割り当てることができます。
つまり、カスタムプログラム用のデータアカウントを作成するには2つのステップが必要です:
- System Programを呼び出してアカウントを作成し、所有権をカスタムプログラムに転送する
- アカウントを所有するようになったカスタムプログラムを呼び出し、プログラムの指示で定義されたアカウントデータを初期化する
このアカウント作成プロセスは多くの場合、単一のステップとして抽象化されていますが、基礎となるプロセスを理解することは役立ちます。
以下の例は、Token 2022プログラムが所有するトークンMintアカウントを作成して取得する方法を示しています。
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,fetchMint} from "@solana-program/token-2022";// Create Connection, local validator in this exampleconst rpc = createSolanaRpc("http://localhost: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);const mintAccount = await fetchMint(rpc, mint.address);console.log(mintAccount);
Is this page helpful?