Tóm tắt
Solana có ba định dạng giao dịch: legacy, v0 và v1. v0 bổ sung Address Lookup Tables (ALTs) để tham chiếu các tài khoản qua chỉ mục 1 byte. v1 nâng giới hạn kích thước lên 4.096 byte, chuyển giới hạn tài nguyên vào trong chính thông điệp và loại bỏ ALTs.
Solana hỗ trợ ba định dạng giao dịch: legacy, v0 và v1. Mỗi định dạng được mô tả bên dưới với ba phần giống nhau: cách bố trí byte trên đường truyền, cách tham chiếu tài khoản và nguồn gốc của các giới hạn tài nguyên.
Trạng thái kích hoạt của v1
Định dạng v1 đã được kích hoạt trên mainnet, devnet và testnet. Việc đọc giao dịch là một thay đổi gây phá vỡ tương thích: các client không chọn tham gia (opt in) sẽ thất bại với giao dịch v1. Các ứng dụng hiện có nên xem chuẩn bị cho v1.
So sánh định dạng
| Giới hạn | legacy | v0 | v1 |
|---|---|---|---|
| Kích thước giao dịch tối đa | 1.232 byte | 1.232 byte | 4.096 byte |
| Địa chỉ tài khoản | ~32, bị giới hạn bởi kích thước | 64, qua lookup tables | 64, nội tuyến (96 với SIMD-0596) |
| Address lookup tables | không hỗ trợ | hỗ trợ | không hỗ trợ |
| Giới hạn tài nguyên | Lệnh ComputeBudget | Lệnh ComputeBudget | cấu hình thông điệp |
Định dạng legacy
Định dạng gốc và vẫn là mặc định trong hầu hết các công cụ. Định dạng này không có
bất kỳ tiền tố phiên bản nào: byte đầu tiên của giao dịch là số đếm compact-u16 của
mảng chữ ký, và byte đầu tiên của thông điệp là num_required_signatures,
trong đó bit cao luôn không được đặt.
Bố cục wire của legacy
| Trường | Kích thước | Mô tả |
|---|---|---|
num_signatures | compact-u16 | Số lượng chữ ký |
signatures | num_signatures x 64 byte | Chữ ký Ed25519 |
header | 3 byte | MessageHeader — byte đầu tiên có bit phiên bản không được đặt |
num_account_keys | compact-u16 | Số lượng khóa tài khoản |
account_keys | num_account_keys x 32 byte | Khóa công khai, tất cả nội tuyến |
recent_blockhash | 32 byte | Bộ xác định vòng đời |
num_instructions | compact-u16 | Số lượng lệnh |
instructions | biến đổi | Mỗi lệnh được tuần tự hóa liên tiếp |
Mỗi mảng có độ dài biến đổi đều được đứng trước bởi độ dài compact-u16: 1 byte cho giá trị 0–127, 2–3 byte cho các giá trị lớn hơn. Để xem bố cục từng lệnh và cách tính kích thước, hãy xem định dạng nhị phân giao dịch.
Tài khoản trong legacy
Mỗi tài khoản được ghi đầy đủ dưới dạng khóa công khai 32 byte trong account_keys,
và các lệnh tham chiếu chúng bằng chỉ mục 1 byte vào mảng đó. Không có cách nào để
tham chiếu một tài khoản không được ghi rõ trong giao dịch, đó là lý do giới hạn
giao dịch legacy ở khoảng 32 tài khoản trước khi vượt quá 1.232 byte.
Giới hạn tài nguyên trong legacy
Giới hạn đơn vị tính toán, giới hạn kích thước dữ liệu tài khoản được nạp, kích thước heap và phí ưu tiên đều được yêu cầu bằng cách đưa các lệnh chương trình ComputeBudget vào giao dịch. Mỗi lệnh chiếm một instruction slot và 150 đơn vị tính toán. Bỏ qua chúng vẫn an toàn: runtime sẽ dùng các giá trị mặc định là 200.000 CU mỗi lệnh (giới hạn tối đa 1,4M), giới hạn kích thước dữ liệu 64 MiB, heap 32 KiB và phí ưu tiên bằng không.
Định dạng V0
v0 là một thông điệp legacy cộng thêm hai thứ: byte tiền tố phiên bản 0x80 và mảng
address_table_lookups được thêm vào sau các lệnh. Mọi thứ trước đó đều giống hệt byte với legacy.
Bố cục wire của V0
| Trường | Kích thước | Mô tả |
|---|---|---|
num_signatures | compact-u16 | Số lượng chữ ký |
signatures | num_signatures x 64 byte | Chữ ký Ed25519 |
0x80 | 1 byte | Byte tiền tố phiên bản — byte đầu tiên của thông điệp |
header | 3 byte | MessageHeader (giống như legacy) |
num_account_keys | compact-u16 | Số lượng khóa tài khoản tĩnh |
static_account_keys | num_account_keys x 32 byte | Các khóa xuất hiện trực tiếp trong giao dịch |
recent_blockhash | 32 byte | Bộ xác định vòng đời |
num_instructions | compact-u16 | Số lượng lệnh |
instructions | biến đổi | Định dạng giống như legacy |
address_table_lookups | compact-u16 + biến thiên | Tham chiếu ALT (xem bên dưới) |
Mỗi mục tra cứu bảng địa chỉ chứa:
| Trường | Kích thước | Mô tả |
|---|---|---|
account_key | 32 byte | Khóa công khai của tài khoản ALT |
writable_indexes | compact-u16 + N x 1 byte | Chỉ số trong ALT cho các tài khoản có thể ghi |
readonly_indexes | compact-u16 + N x 1 byte | Chỉ số trong ALT cho các tài khoản chỉ đọc |
Address lookup tables
ALT là một tài khoản onchain lưu trữ tối đa 256 khóa công khai. Bằng cách tham chiếu đến một ALT, một giao dịch có thể bao gồm các tài khoản bổ sung bằng cách sử dụng các chỉ số 1 byte thay vì khóa công khai 32 byte, giảm đáng kể chi phí cho mỗi tài khoản.
Tại thời điểm chạy, trước khi bắt đầu thực thi, validator phân giải tất cả các tham chiếu ALT thành khóa công khai đầy đủ. Các địa chỉ đã phân giải được thêm vào các khóa tài khoản tĩnh để tạo thành danh sách khóa tài khoản hoàn chỉnh. Các tài khoản được phân giải từ ALT tuân theo thứ tự tương tự như các tài khoản tĩnh: các tra cứu có thể ghi đứng trước các tra cứu chỉ đọc.
Bảng tra cứu địa chỉ chỉ ảnh hưởng đến cách các tài khoản được tham chiếu trong giao dịch trên mạng. Tại thời điểm thực thi, runtime phân giải tất cả các chỉ số thành địa chỉ tài khoản đầy đủ. Các tài khoản được phân giải từ ALT chỉ có thể là có thể ghi hoặc chỉ đọc (không phải người ký); chúng không thể là người ký.
Tạo và sử dụng address lookup table
Ví dụ này tạo một lookup table, thêm một địa chỉ, và dùng bảng đó để biên dịch và gửi một giao dịch v0.
Chỉ các tài khoản không phải signer mới có thể được tải từ lookup table. Bên trả phí và mọi signer bắt buộc vẫn nằm trong danh sách tài khoản tĩnh của thông điệp. Một địa chỉ cũng không thể được tra cứu trong cùng slot mà nó được thêm vào, vì vậy ví dụ chờ slot đã xác nhận tiếp theo trước khi biên dịch thông điệp v0.
Giới hạn tài nguyên trong v0
Không thay đổi so với legacy: các lệnh ComputeBudget, với các giá trị mặc định tương tự khi bị bỏ qua.
Định dạng V1
v1 nâng giới hạn kích thước lên 4.096 byte và tái cơ cấu thông điệp xung quanh một cấu hình giao dịch: giới hạn tài nguyên được chuyển ra khỏi các lệnh ComputeBudget vào các trường có vị trí cố định trong chính thông điệp. Điều này cho phép mạng xếp hạng một giao dịch theo phí ưu tiên bằng một lần đọc offset cố định thay vì quét và giải tuần tự hóa danh sách lệnh.
Bố cục wire của V1
| Trường | Kích thước | Mô tả |
|---|---|---|
0x81 | 1 byte | Byte tiền tố phiên bản — byte đầu tiên của giao dịch |
header | 3 byte | MessageHeader (giống như legacy) |
config_mask | 4 byte | Bitmask u32 LE đánh dấu các giá trị cấu hình nào hiện diện |
recent_blockhash | 32 byte | Bộ xác định vòng đời |
num_instructions | 1 byte | Số đếm độ rộng cố định, tối đa 64 |
num_addresses | 1 byte | Số đếm độ rộng cố định, tối đa 64 (96 với SIMD-0596) |
addresses | N x 32 byte | Địa chỉ tài khoản, tất cả nội tuyến — không có tham chiếu lookup table |
config_values | 0–20 byte | Một giá trị cho mỗi bit mask được đặt, theo thứ tự bit (xem bên dưới) |
instruction_headers | N x 4 byte | Mỗi lệnh: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | biến đổi | Mỗi lệnh: các chỉ mục tài khoản, sau đó là instruction data |
signatures | N x 64 byte | Ở cuối, không có tiền tố độ dài — số đếm lấy từ header |
Hai điểm khác biệt về cấu trúc so với legacy và v0 đáng chú ý khi viết
bộ giải mã. Các số đếm là trường u8 độ rộng cố định thay vì compact-u16, và
các lệnh được chia thành hai phần: tất cả header kích thước cố định trước, sau đó là tất cả
payload độ dài biến đổi, thay vì mỗi lệnh được liên tiếp.
Tài khoản trong v1: không có address lookup tables
v1 cố ý loại bỏ hỗ trợ ALT. 64 địa chỉ thô là 2.048 byte, và 96 địa chỉ được phép sau SIMD-0596 là 3.072 byte, cả hai đều nằm gọn trong giới hạn 4.096 byte, nên mọi địa chỉ đều nằm nội tuyến như trong legacy. Nếu ứng dụng của bạn phụ thuộc vào lookup table, việc chuyển sang v1 đồng nghĩa với việc đưa các địa chỉ đó vào nội tuyến.
Giới hạn tài nguyên trong v1: cấu hình giao dịch
Cấu hình là một bitmask u32 theo sau là các giá trị độ rộng cố định cho từng trường
có bit được đặt:
| Bit | Trường | Độ rộng | Ghi chú |
|---|---|---|---|
| 0–1 | Phí ưu tiên | u64 | Tổng lamport — cả hai bit được đặt cùng nhau |
| 2 | Giới hạn đơn vị tính toán | u32 | |
| 3 | Giới hạn kích thước dữ liệu tài khoản được nạp | u32 | |
| 4 | Kích thước heap được yêu cầu | u32 |
Các bit không xác định sẽ bị từ chối. Vì thông điệp được ký, các trường cấu hình không nhận dạng được không thể bị loại bỏ âm thầm.
Phí ưu tiên là tổng lamport, không phải đơn giá
Trong legacy và v0, phí ưu tiên được đặt qua SetComputeUnitPrice dưới dạng
micro-lamport trên mỗi đơn vị tính toán, nhân với giới hạn đơn vị tính toán. Trong
v1 đây là tổng tuyệt đối tính bằng lamport — không nhân, không làm tròn.
Đừng mang phép tính theo CU sang. Công thức tính tổng phí không thay đổi:
(signatures × lamports_per_signature) + priority_fee.
Các lệnh ComputeBudget là no-op trong v1
Một giao dịch v1 không từ chối các lệnh ComputeBudget — nó bỏ qua chúng cho mục đích cấu hình. Chúng vẫn thực thi thành công dưới dạng no-op, tiêu thụ 150 đơn vị tính toán và một trong 64 instruction slot trong khi không có tác dụng gì đến ngân sách. Hãy loại bỏ chúng khi xây dựng giao dịch v1, và ngừng quét tìm chúng khi đọc giao dịch v1: các giá trị nằm trong cấu hình thông điệp.
Các trường cấu hình phải được đặt rõ ràng
Thay đổi hành vi quan trọng nhất đối với người gửi: không giống như legacy và v0, giao dịch v1 phải đặt rõ ràng giới hạn đơn vị tính toán và giới hạn kích thước dữ liệu tài khoản được nạp, nếu không giao dịch sẽ thất bại.
| Trường chưa đặt | legacy / v0 | v1 | Triệu chứng nếu bị bỏ qua |
|---|---|---|---|
| Giới hạn đơn vị tính toán | 200k mỗi lệnh, tối đa 1,4M | 0 CU | Thất bại ngay lập tức, hết ngân sách |
| Kích thước dữ liệu tài khoản được nạp | 64 MiB | 0 byte | MaxLoadedAccountsDataSizeExceeded ở tài khoản đầu tiên được nạp |
| Phí ưu tiên | 0 | 0 | — |
| Kích thước heap | 32 KiB | 32 KiB | — |
Cách tiếp cận được khuyến nghị là mô phỏng một lần với cả hai giới hạn ở mức tối đa, sau đó ghi
unitsConsumed và loadedAccountsDataSize được trả về vào cấu hình,
làm tròn kích thước dữ liệu lên trang 32 KiB tiếp theo để có biên độ dự phòng (mô hình
chi phí block tính theo các trang 32 KiB, vì vậy biên độ dự phòng dưới ranh giới trang tiếp theo là
miễn phí).
Xây dựng giao dịch v1
Một thông điệp v1 được lắp ráp qua cùng một pipeline như thông điệp legacy hoặc v0. Điểm
khác biệt là setTransactionMessageConfig: các giới hạn tài nguyên là thuộc tính
của thông điệp, nên danh sách lệnh chỉ chứa các lệnh của chương trình.
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageConfig,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// A v1 message is assembled through the same pipeline as a legacy or v0 one.// The difference is the config: a v1 transaction carries its resource limits in// the message itself, so no ComputeBudget instructions are compiled in.const transactionMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),// The compute unit limit and loaded accounts data size limit both default to// zero on v1, so a transaction that leaves them unset always fails. The// priority fee is an absolute total in lamports, not a price per compute// unit.(m) =>setTransactionMessageConfig({computeUnitLimit: 20_000,loadedAccountsDataSizeLimit: 64 * 1024,priorityFeeLamports: 5_000n,},m,),);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);console.log("Transaction Signature:", transactionSignature);
Kit cũng cung cấp một setter cho từng trường cấu hình —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit, và
setTransactionMessageHeapSize — để thiết lập từng giá trị một.
Xác định kích thước giới hạn bằng mô phỏng
Thay vì đoán hai giới hạn bắt buộc, hãy đo chúng. Mô phỏng chạy với cả hai giới hạn được nâng lên mức tối đa của runtime, nên nó không thể thất bại vì thiếu tài nguyên mà nó đang đo. Con số trả về là chi phí chính xác của một lần chạy mô phỏng, vì vậy hãy cộng thêm một khoảng dự phòng trước khi dựa vào nó.
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,estimateAndSetResourceLimitsFactory,estimateResourceLimitsFactory,fillTransactionMessageProvisoryResourceLimits,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,setTransactionMessagePriorityFeeLamports,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// The priority fee is a pricing decision that simulation cannot measure, so it// is set directly. The two resource limits are measured below instead of// guessed. The provisory fill writes placeholder limits so the message// simulates at the same size it will be sent at.const draftMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),(m) => setTransactionMessagePriorityFeeLamports(5_000n, m),fillTransactionMessageProvisoryResourceLimits,);// Simulation runs with both limits raised to the runtime maximum, so it cannot// fail for want of the resources it is measuring.const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(estimateResourceLimitsFactory({ rpc: client.rpc }),);const transactionMessage = await estimateAndSetResourceLimits(draftMessage, {commitment: "confirmed",});console.log("Compute unit limit:", transactionMessage.config?.computeUnitLimit);console.log("Loaded accounts data size limit:",transactionMessage.config?.loadedAccountsDataSizeLimit,);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });// The estimate is the exact cost of one simulated run, with nothing to spare.// Add a margin before relying on it in production.console.log("Transaction Signature:",getSignatureFromTransaction(signedTransaction),);
Chuẩn bị cho v1
v1 thay đổi việc đọc giao dịch, chứ không chỉ việc gửi. Bất kỳ client nào
gọi getTransaction hoặc getBlock mà không chọn tham gia (opt in) đều thất bại với giao dịch v1:
- Truyền
maxSupportedTransactionVersion: 1— số nguyên JSON là1, không phải chuỗi"1"— chogetTransactionvàgetBlock. Truyền0sẽ thất bại trên giao dịch v1 giống như bỏ qua tham số, vì vậy một codebase đã được cập nhật trong quá trình triển khai v0 vẫn cần thay đổi giá trị này. getTransactionthất bại trên giao dịch v1 với lỗi-32015, và một giao dịch v1 khiến toàn bộ phản hồigetBlockthất bại với cùng lỗi — không có kết quả một phần.blockSubscribephát rablock: nullvà ngừng tiến, vì vậy một consumer đọc điều đó như một block rỗng sẽ âm thầm bị tụt lại từ slot v1 đầu tiên trở đi.getSignaturesForAddresskhông bao giờ kiểm tra nội dung giao dịch, vì vậy các chữ ký v1 được liệt kê bình thường.- Các phản hồi đã chọn tham gia có kèm đối tượng
transactionConfigtrong thông điệp đối với giao dịch v1 (hoàn toàn không có đối với legacy và v0). Các pipeline suy ra phí ưu tiên hoặc giới hạn tính toán bằng cách quét các lệnh ComputeBudget sẽ âm thầm báo cáo không cho mọi giao dịch v1. - Sử dụng
encoding: 'base64'khi bạn giải mã giao dịch phía client, và chosendTransaction/simulateTransactionvới các giao dịch vượt quá 1.232 byte — mã hóa base58 vẫn bị giới hạn ở kích thước cũ. - Hỗ trợ thư viện client yêu cầu các phiên bản gần đây:
@solana/kit8.0+, các crate Rust thế hệ Agave 4.2.x, hoặc web3.js v3. web3.js v1 đọc v1 từ phiên bản 1.99.0 trở đi, nhưng không thể xây dựng hoặc gửi v1.
Đọc một giao dịch v1
Việc chọn tham gia dùng số nguyên JSON 1. Giải mã các byte trả về sẽ cho ra
phiên bản và cấu hình, nơi chứa các giới hạn tài nguyên — quét
danh sách lệnh để tìm các lệnh ComputeBudget sẽ không thấy gì.
import {decompileTransactionMessage,getBase64Encoder,getCompiledTransactionMessageDecoder,getTransactionDecoder,} from "@solana/kit";// Reading a v1 transaction requires opting in. The integer 1 is mandatory:// omitting the parameter, or passing 0, fails on a v1 transaction rather than// returning it in a degraded form.const fetched = await client.rpc.getTransaction(transactionSignature, {commitment: "confirmed",encoding: "base64",maxSupportedTransactionVersion: 1,}).send();if (fetched === null) {throw new Error("the transaction just sent was not found");}// The compiled message stores the config as a bitmask plus a positional list,// so it is decompiled to address the fields by name. Decompiling fetches no// accounts, because v1 does not support address lookup tables.const wireTransaction = getBase64Encoder().encode(fetched.transaction[0]);const transaction = getTransactionDecoder().decode(wireTransaction);const compiledMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes,);const message = decompileTransactionMessage(compiledMessage);console.log("Version:", message.version);if ("config" in message) {// Scanning the instruction list for ComputeBudget instructions finds nothing// here — on v1 the limits live in the config.console.log("Compute unit limit:", message.config?.computeUnitLimit);console.log("Priority fee (lamports):", message.config?.priorityFeeLamports);}
Để xem hướng dẫn di chuyển đầy đủ — bao gồm hỗ trợ thư viện client, phát hiện phiên bản streaming (Geyser/gRPC), và hành vi mô phỏng — hãy xem trang nâng cấp Transaction Format v1. Các ví dụ có thể chạy được bằng TypeScript, Rust, Python và Go nằm trong solana-foundation/transaction-v1-examples.
Is this page helpful?