Mô hình tài khoản Solana
Trên Solana, tất cả dữ liệu được lưu trữ trong cái gọi là "accounts" (tài khoản). Bạn có thể coi dữ liệu trên Solana như một cơ sở dữ liệu công khai với một bảng "Accounts" duy nhất, trong đó mỗi mục trong bảng này là một "account". Mọi tài khoản Solana đều chia sẻ cùng một kiểu Account cơ bản.
Tài khoản
Điểm chính
- Tài khoản có thể lưu trữ tối đa 10MiB dữ liệu, chứa mã chương trình thực thi hoặc trạng thái chương trình.
- Tài khoản yêu cầu một khoản đặt cọc rent bằng lamport (SOL) tỷ lệ thuận với lượng dữ liệu được lưu trữ, và bạn có thể thu hồi toàn bộ khi đóng tài khoản.
- Mỗi tài khoản có một chương trình sở hữu. Chỉ chương trình sở hữu tài khoản mới có thể thay đổi dữ liệu hoặc trừ số dư lamport của nó. Nhưng bất kỳ ai cũng có thể tăng số dư.
- Tài khoản Sysvar là những tài khoản đặc biệt lưu trữ trạng thái cụm mạng.
- Tài khoản chương trình lưu trữ mã thực thi của các hợp đồng thông minh.
- Tài khoản dữ liệu được tạo bởi các chương trình để lưu trữ và quản lý trạng thái chương trình.
Tài khoản
Mỗi tài khoản trên Solana có một địa chỉ duy nhất 32 byte, thường được hiển thị
dưới dạng chuỗi mã hóa base58 (ví dụ:
vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo6NmXEyxm
).
Mối quan hệ giữa tài khoản và địa chỉ của nó hoạt động như một cặp key-value, trong đó địa chỉ là khóa để định vị dữ liệu on-chain tương ứng của tài khoản. Địa chỉ tài khoản đóng vai trò như "ID duy nhất" cho mỗi mục trong bảng "Accounts".
Địa chỉ tài khoản
Hầu hết các tài khoản Solana sử dụng khóa công khai Ed25519 làm địa chỉ của chúng.
import { generateKeyPairSigner } from "@solana/kit";// Kit does not enable extractable private keysconst keypairSigner = await generateKeyPairSigner();console.log(keypairSigner);
Mặc dù các khóa công khai thường được sử dụng làm địa chỉ tài khoản, Solana cũng hỗ trợ một tính năng gọi là Địa chỉ phái sinh từ chương trình (PDAs). PDAs là những địa chỉ đặc biệt mà bạn có thể tạo ra một cách xác định từ ID chương trình và các đầu vào tùy chọn (seeds).
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}`);
Loại tài khoản
Tài khoản có kích thước tối đa là 10MiB và mọi tài khoản trên Solana đều chia sẻ cùng một loại Account cơ bản.
Loại tài khoản
Mọi tài khoản trên Solana đều có các trường sau.
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,}
Trường Lamports
Số dư tài khoản tính bằng lamport, đơn vị nhỏ nhất của SOL (1 SOL = 1 tỷ
lamport). Số dư SOL của một tài khoản là số lượng trong trường lamports
được
chuyển đổi sang SOL.
Các tài khoản Solana phải có số dư lamport tối thiểu tỷ lệ thuận với lượng dữ liệu được lưu trữ trên tài khoản (tính bằng byte). Số dư tối thiểu này được gọi là "rent".
Số dư lamport được lưu trữ trong tài khoản có thể được khôi phục hoàn toàn khi tài khoản được đóng.
Trường dữ liệu
Một mảng byte lưu trữ dữ liệu tùy ý cho một tài khoản. Trường dữ liệu này thường được gọi là "dữ liệu tài khoản."
- Đối với tài khoản program (hợp đồng thông minh), trường này chứa mã chương trình thực thi hoặc địa chỉ của một tài khoản khác lưu trữ mã chương trình thực thi.
- Đối với tài khoản không thực thi, trường này thường lưu trữ trạng thái được dùng để đọc.
Đọc dữ liệu từ một tài khoản Solana bao gồm hai bước:
- Lấy tài khoản bằng địa chỉ của nó (khóa công khai)
- Giải mã trường dữ liệu của tài khoản từ các byte thô thành cấu trúc dữ liệu phù hợp, được xác định bởi program sở hữu tài khoản
Trường Owner
ID của program (khóa công khai) của program sở hữu tài khoản này.
Mỗi tài khoản Solana có một program được chỉ định làm chủ sở hữu. Chỉ program sở hữu tài khoản mới có thể thay đổi dữ liệu của tài khoản hoặc trừ số dư lamport của nó.
Các chỉ thị được định nghĩa trong một program xác định cách thức dữ liệu và số dư lamport của tài khoản có thể được thay đổi.
Trường Executable
Trường này chỉ ra liệu một tài khoản có phải là một program thực thi hay không.
- Nếu
true
, tài khoản là một program Solana thực thi. - Nếu
false
, tài khoản là một tài khoản dữ liệu lưu trữ trạng thái.
Đối với tài khoản thực thi, trường owner
chứa ID program của một program tải.
Các program tải là các program tích hợp sẵn chịu trách nhiệm tải và quản lý các
tài khoản program thực thi.
Trường Rent Epoch
Trường rent_epoch
là một trường cũ không còn được sử dụng nữa.
Ban đầu, trường này theo dõi khi nào một tài khoản cần phải trả rent (bằng lamport) để duy trì dữ liệu của nó trên mạng. Tuy nhiên, cơ chế thu rent này đã bị loại bỏ.
Rent
Để lưu trữ dữ liệu trên chuỗi, các tài khoản cũng phải duy trì số dư lamport (SOL) tỷ lệ thuận với lượng dữ liệu được lưu trữ trên tài khoản (tính bằng byte). Số dư này được gọi là "rent", nhưng nó hoạt động giống như một khoản đặt cọc vì bạn có thể lấy lại toàn bộ số tiền khi đóng tài khoản. Bạn có thể tìm thấy cách tính toán tại đây sử dụng các hằng số.
Thuật ngữ "rent" bắt nguồn từ một cơ chế đã lỗi thời trước đây thường xuyên khấu trừ lamport từ các tài khoản có số dư thấp hơn ngưỡng rent. Cơ chế này hiện không còn hoạt động nữa.
Chủ sở hữu chương trình
Trên Solana, "hợp đồng thông minh" được gọi là chương trình. Quyền sở hữu chương trình là một phần quan trọng trong Mô hình Tài khoản Solana. Mỗi tài khoản có một chương trình được chỉ định làm chủ sở hữu. Chỉ chương trình chủ sở hữu mới có thể:
- Thay đổi trường
data
của tài khoản - Khấu trừ lamport từ số dư của tài khoản
Mỗi chương trình xác định cấu trúc dữ liệu được lưu trữ trong trường data
của
tài khoản. Các hướng dẫn của chương trình xác định cách thức dữ liệu này và số
dư lamports
của tài khoản có thể được thay đổi.
System Program
Theo mặc định, tất cả tài khoản mới đều thuộc sở hữu của System Program. System Program thực hiện các chức năng chính sau đây:
Chức năng | Mô tả |
---|---|
Tạo tài khoản mới | Chỉ System Program mới có thể tạo tài khoản mới. |
Phân bổ không gian | Thiết lập dung lượng byte cho trường dữ liệu của mỗi tài khoản. |
Gán quyền sở hữu chương trình | Sau khi System Program tạo tài khoản, nó có thể gán lại chương trình chủ sở hữu cho một tài khoản chương trình khác. Đó là cách các chương trình tùy chỉnh tiếp quản quyền sở hữu các tài khoản mới được tạo bởi System Program. |
Chuyển SOL | Chuyển lamport (SOL) từ các tài khoản System sang các tài khoản khác. |
Lưu ý rằng tất cả các tài khoản "ví" trên Solana đều là "System Accounts" thuộc sở hữu của System Program. Số dư lamport trong các tài khoản này thể hiện lượng SOL mà ví sở hữu. Chỉ có System Accounts mới có thể thanh toán phí giao dịch.
System Account
Khi SOL được gửi đến một địa chỉ mới lần đầu tiên, một tài khoản sẽ tự động được tạo tại địa chỉ đó thuộc sở hữu của System Program.
Trong ví dụ dưới đây, một keypair mới được tạo và nạp SOL. Chạy mã để xem kết
quả. Lưu ý rằng trường owner
của tài khoản là System Program với địa chỉ
11111111111111111111111111111111
.
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);
Tài khoản Sysvar
Tài khoản Sysvar là các tài khoản đặc biệt tại các địa chỉ được xác định trước, cung cấp quyền truy cập vào dữ liệu trạng thái của cụm mạng. Các tài khoản này cập nhật động với dữ liệu về cụm mạng. Bạn có thể tìm thấy danh sách đầy đủ các tài khoản Sysvar tại đây.
Ví dụ sau đây cho thấy cách lấy và giải mã dữ liệu từ tài khoản 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
Triển khai một chương trình Solana sẽ tạo ra một program account có thể thực thi. Program account lưu trữ mã thực thi của chương trình. Program accounts thuộc sở hữu của Loader Program.
Program Account
Để đơn giản, bạn có thể coi program account là chính chương trình đó. Khi bạn gọi các lệnh của một chương trình, bạn chỉ định địa chỉ của program account (thường được gọi là "Program ID").
Ví dụ sau đây lấy Token Program account để cho thấy rằng program accounts có
cùng kiểu cơ sở Account
, ngoại trừ trường executable
được đặt thành true
.
Vì program accounts chứa mã thực thi trong trường dữ liệu của chúng, chúng ta
không giải mã dữ liệu.
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);
Khi bạn triển khai một chương trình Solana, nó được lưu trữ trong một program account. Program accounts được sở hữu bởi một Chương trình tải. Có nhiều phiên bản của trình tải, nhưng tất cả ngoại trừ loader-v3 đều lưu trữ mã thực thi trực tiếp trong program account. Loader-v3 lưu trữ mã thực thi trong một "program data account" riêng biệt và program account chỉ trỏ đến nó. Khi bạn triển khai một chương trình mới, Solana CLI sẽ sử dụng phiên bản trình tải mới nhất theo mặc định.
Tài khoản đệm (Buffer Account)
Loader-v3 có một loại tài khoản đặc biệt để tạm thời lưu trữ việc tải lên chương trình trong quá trình triển khai hoặc nâng cấp. Trong loader-v4, vẫn có các bộ đệm, nhưng chúng chỉ là các program account thông thường.
Tài khoản dữ liệu chương trình (Program Data Account)
Loader-v3 hoạt động khác với tất cả các chương trình BPF Loader khác. Program account chỉ chứa địa chỉ của một program data account, nơi lưu trữ mã thực thi thực sự:
Program Data Account
Đừng nhầm lẫn các program data account này với các data account của chương trình (xem bên dưới).
Tài khoản dữ liệu (Data Account)
Trên Solana, mã thực thi của một chương trình được lưu trữ trong một tài khoản khác với trạng thái của chương trình. Điều này giống như cách các hệ điều hành thường có các tệp riêng biệt cho chương trình và dữ liệu của chúng.
Để duy trì trạng thái, các chương trình định nghĩa các hướng dẫn để tạo các tài khoản riêng biệt mà chúng sở hữu. Mỗi tài khoản này có địa chỉ duy nhất riêng và có thể lưu trữ bất kỳ dữ liệu tùy ý nào được định nghĩa bởi chương trình.
Data Account
Lưu ý rằng chỉ có System Program mới có thể tạo tài khoản mới. Sau khi System Program tạo một tài khoản, nó có thể gán quyền sở hữu của tài khoản mới cho một chương trình khác.
Nói cách khác, việc tạo một tài khoản dữ liệu cho một chương trình tùy chỉnh cần hai bước:
- Gọi System Program để tạo một tài khoản, sau đó chuyển quyền sở hữu cho chương trình tùy chỉnh
- Gọi chương trình tùy chỉnh, hiện đang sở hữu tài khoản, để khởi tạo dữ liệu tài khoản theo định nghĩa của hướng dẫn chương trình
Quá trình tạo tài khoản này thường được trừu tượng hóa thành một bước duy nhất, nhưng hiểu rõ quy trình cơ bản là rất hữu ích.
Ví dụ sau đây cho thấy cách tạo và lấy một tài khoản Token Mint được sở hữu bởi chương trình Token 2022.
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?