Giới thiệu về Solana
Trước khi xây dựng trên Solana, bạn cần hiểu một số khái niệm cơ bản làm cho Solana trở nên độc đáo. Hướng dẫn này bao gồm accounts, transactions, programs và các tương tác của chúng.
Các Account trên Solana
Kiến trúc của Solana tập trung xung quanh các account: các thùng chứa dữ liệu lưu trữ thông tin trên blockchain. Hãy nghĩ về các account như các file riêng lẻ trong một hệ thống file, nơi mỗi file có các thuộc tính cụ thể và một owner kiểm soát nó.
Mỗi account Solana có cùng một cấu trúc cơ bản:
pub struct Account {
/// lamports in the account
pub 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 rent
pub rent_epoch: Epoch,
}
Mỗi account có một địa chỉ 32-byte duy nhất, được hiển thị dưới dạng chuỗi được mã hóa base58 (ví dụ: 14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5
). Địa chỉ này phục vụ như định danh của account trên blockchain và là cách bạn định vị dữ liệu cụ thể.
Các account có thể lưu trữ tối đa 10 MiB dữ liệu, có thể chứa code chương trình có thể thực thi hoặc dữ liệu cụ thể của chương trình.
Tất cả các account yêu cầu một khoản tiền gửi lamport tỷ lệ với kích thước dữ liệu của chúng để trở thành "miễn rent." Thuật ngữ "rent" mang tính lịch sử, vì lamport ban đầu được khấu trừ từ các account mỗi epoch, nhưng tính năng này hiện đã bị vô hiệu hóa. Ngày nay, khoản tiền gửi hoạt động giống như một khoản tiền gửi có thể hoàn lại. Miễn là account của bạn duy trì số dư tối thiểu cho kích thước dữ liệu của nó, nó vẫn miễn rent và tồn tại vô thời hạn. Khi bạn không còn cần một account, bạn có thể đóng nó và thu hồi hoàn toàn khoản tiền gửi này.
Mỗi account được sở hữu bởi một chương trình, và chỉ chương trình sở hữu đó mới có thể sửa đổi dữ liệu của account hoặc rút lamport của nó. Tuy nhiên, bất kỳ ai cũng có thể tăng số dư lamport của account, điều này hữu ích để tài trợ cho các hoạt động hoặc trả rent mà không cần dựa vào việc gọi chính chương trình đó.
Quyền ký hoạt động khác nhau tùy thuộc vào quyền sở hữu. Các account được sở hữu bởi System Program có thể ký giao dịch để sửa đổi dữ liệu của chính chúng, chuyển quyền sở hữu, hoặc thu hồi lamport đã lưu trữ. Một khi quyền sở hữu chuyển sang chương trình khác, chương trình đó sẽ có toàn quyền kiểm soát account bất kể bạn có còn sở hữu private key hay không. Việc chuyển quyền kiểm soát này là vĩnh viễn và không thể đảo ngược.
Các loại Account
Loại account phổ biến nhất là System Account, lưu trữ lamport (đơn vị nhỏ nhất của SOL) và được sở hữu bởi System Program. Chúng hoạt động như các account ví cơ bản mà người dùng tương tác trực tiếp để gửi và nhận SOL.
Token Account phục vụ mục đích chuyên biệt, lưu trữ thông tin SPL token, bao gồm quyền sở hữu và metadata token. Token Program sở hữu các account này và quản lý tất cả các hoạt động liên quan đến token trong hệ sinh thái Solana. Token Account là Data Account.
Data Account lưu trữ thông tin cụ thể của ứng dụng và được sở hữu bởi các chương trình tùy chỉnh. Các account này giữ trạng thái của ứng dụng bạn và có thể được cấu trúc theo bất kỳ cách nào mà chương trình của bạn yêu cầu, từ hồ sơ người dùng đơn giản đến dữ liệu tài chính phức tạp.
Cuối cùng, Program Account chứa code có thể thực thi chạy trên Solana, đây là nơi smart contract tồn tại. Các account này được đánh dấu là executable: true
và lưu trữ logic của chương trình xử lý instruction và quản lý trạng thái.
Làm việc với Dữ liệu Account
Đây là cách các chương trình tương tác với dữ liệu account:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
pub name: String,
pub balance: u64,
pub posts: Vec<u32>,
}
pub fn update_user_data(accounts: &[AccountInfo], new_name: String) -> ProgramResult {
let user_account = &accounts[0];
// Deserialize existing data
let mut user_data = UserAccount::try_from_slice(&user_account.data.borrow())?;
// Modify the data
user_data.name = new_name;
// Serialize back to account
user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
Không giống như cơ sở dữ liệu nơi bạn chỉ đơn giản chèn bản ghi, các account Solana phải được tạo và tài trợ một cách rõ ràng trước khi sử dụng.
Các Transaction trên Solana
Các transaction Solana là các thao tác nguyên tử có thể chứa nhiều instruction. Tất cả các instruction trong một transaction hoặc thành công cùng nhau hoặc thất bại cùng nhau: không có thực thi một phần.
Một transaction bao gồm:
- Instruction: Các thao tác riêng lẻ cần thực hiện
- Account: Các account cụ thể mà mỗi instruction sẽ đọc từ hoặc ghi vào
- Signer: Các account ủy quyền cho transaction
Transaction {
instructions: [
// Instruction 1: Transfer SOL
system_program::transfer(from_wallet, to_wallet, amount),
// Instruction 2: Update user profile
my_program::update_profile(user_account, new_name),
// Instruction 3: Log activity
my_program::log_activity(activity_account, "transfer", amount),
],
accounts: [from_wallet, to_wallet, user_account, activity_account]
signers: [user_keypair],
}
Yêu cầu và Phí Transaction
Các transaction bị giới hạn tổng cộng 1,232 byte, điều này hạn chế số lượng instruction và account bạn có thể bao gồm.
Mỗi instruction trong một transaction yêu cầu ba thành phần thiết yếu: địa chỉ chương trình để gọi, tất cả các account mà instruction sẽ đọc từ hoặc ghi vào, và bất kỳ dữ liệu bổ sung nào như tham số hàm.
Các instruction thực thi tuần tự theo thứ tự bạn chỉ định trong transaction.
Mỗi transaction phải chịu phí cơ bản 5,000 lamport cho mỗi chữ ký để bù đắp cho các validator xử lý transaction của bạn.
Bạn cũng có thể trả phí ưu tiên tùy chọn để tăng khả năng leader hiện tại xử lý transaction của bạn một cách nhanh chóng. Phí ưu tiên này được tính bằng giới hạn compute unit của bạn nhân với giá compute unit của bạn (đo bằng micro-lamport).
prioritization_fee = compute_unit_limit × compute_unit_price
Các Program trên Solana
Các program trên Solana về cơ bản là stateless, có nghĩa là chúng không duy trì bất kỳ trạng thái nội bộ nào giữa các lời gọi hàm. Thay vào đó, chúng nhận các account làm đầu vào, xử lý dữ liệu trong những account đó, và trả về kết quả đã được sửa đổi.
Thiết kế stateless này đảm bảo hành vi có thể dự đoán và cho phép các mẫu khả năng kết hợp mạnh mẽ.
Bản thân các program được lưu trữ trong các account đặc biệt được đánh dấu là executable: true
, chứa code binary đã biên dịch thực thi khi được gọi.
Người dùng tương tác với những program này bằng cách gửi các transaction chứa các instruction cụ thể, mỗi instruction nhắm đến các hàm program cụ thể với dữ liệu account và tham số cần thiết.
use solana_program::prelude::*;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
pub name: String,
pub created_at: i64,
}
pub fn create_user(
accounts: &[AccountInfo],
name: String,
) -> ProgramResult {
let user_account = &accounts[0];
let user = User {
name,
created_at: Clock::get()?.unix_timestamp,
};
user.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
Các program có thể được cập nhật bởi upgrade authority được chỉ định của chúng, cho phép các nhà phát triển sửa lỗi và thêm tính năng sau khi triển khai. Tuy nhiên, việc loại bỏ upgrade authority này làm cho program trở nên bất biến vĩnh viễn, cung cấp cho người dùng đảm bảo rằng code sẽ không bao giờ thay đổi.
Để minh bạch và bảo mật, người dùng có thể xác minh rằng các program onchain khớp với source code công khai của chúng thông qua các bản build có thể xác minh, đảm bảo bytecode đã triển khai tương ứng chính xác với source đã xuất bản.
Program Derived Address (PDA)
PDA là các địa chỉ được tạo ra một cách xác định cho phép các mẫu khả năng lập trình mạnh mẽ. Chúng được tạo bằng cách sử dụng seed và program ID, tạo ra các địa chỉ mà không có private key tương ứng.
PDA sử dụng hàm băm SHA-256
với các đầu vào cụ thể, bao gồm seed tùy chỉnh của bạn, giá trị bump để đảm bảo kết quả là không nằm trên đường cong elliptic, program ID sẽ sở hữu PDA, và một marker không đổi.
Khi hash tạo ra một địa chỉ nằm trên đường cong elliptic (điều này xảy ra khoảng 50% thời gian), hệ thống lặp từ bump 255 xuống 254, 253, và cứ thế cho đến khi tìm thấy kết quả nằm ngoài đường cong.
use solana_nostd_sha256::hashv;
const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress";
let pda = hashv(&[
seed_data.as_ref(), // Your custom seeds
&[bump], // Bump to ensure off-curve
program_id.as_ref(), // Program that owns this PDA
PDA_MARKER,
]);
Lợi ích
Bản chất xác định của PDA loại bỏ nhu cầu lưu trữ địa chỉ: bạn có thể tái tạo chúng từ cùng các seed bất cứ khi nào cần.
Điều này tạo ra các sơ đồ địa chỉ có thể dự đoán hoạt động như cấu trúc hashmap onchain. Quan trọng hơn, các program có thể ký cho PDA của riêng chúng, cho phép quản lý tài sản tự động mà không cần tiết lộ private key:
let seeds = &[b"vault", user.as_ref(), &[bump]];
invoke_signed(
&transfer_instruction,
&[from_pda, to_account, token_program],
&[&seeds[..]], // Program proves PDA control
)?;
Cross Program Invocation (CPI)
CPI cho phép các program gọi các program khác trong cùng một transaction, tạo ra khả năng kết hợp thực sự nơi nhiều program có thể tương tác một cách nguyên tử mà không cần điều phối bên ngoài.
Điều này cho phép các nhà phát triển xây dựng các ứng dụng phức tạp bằng cách kết hợp các program hiện có thay vì xây dựng lại chức năng từ đầu.
CPI tuân theo cùng một mẫu như các instruction thông thường, yêu cầu bạn chỉ định program đích, các account mà nó cần, và dữ liệu instruction, với sự khác biệt chính là chúng có thể được thực hiện bên trong các program khác.
Program đang được gọi duy trì quyền kiểm soát luồng trong khi ủy thác các thao tác cụ thể cho các program chuyên biệt:
let cpi_accounts = TransferAccounts {
from: source_account.clone(),
to: destination_account.clone(),
authority: authority_account.clone(),
};
let cpi_ctx = CpiContext::new(token_program.clone(), cpi_accounts);
token_program::cpi::transfer(cpi_ctx, amount)?;
Ràng buộc và Khả năng
Các signer của transaction gốc duy trì quyền hạn của họ trong suốt chuỗi CPI, cho phép các program hành động thay mặt người dùng một cách liền mạch.
Tuy nhiên, các program chỉ có thể thực hiện CPI sâu tối đa 4 cấp (A → B → C → D
) để ngăn chặn đệ quy vô hạn. Các program cũng có thể ký cho PDA của chúng trong CPI bằng cách sử dụng CpiContext::new_with_signer
, cho phép các thao tác tự động phức tạp.
Khả năng kết hợp này cho phép các thao tác phức tạp trên nhiều program trong một transaction nguyên tử duy nhất, làm cho các ứng dụng Solana có tính mô-đun cao và có thể tương tác.