Riepilogo
Solana dispone di tre formati di transazione: legacy, v0 e v1. v0 aggiunge le Address Lookup Table (ALT) per referenziare gli account tramite indici a 1 byte. v1 porta il limite di dimensione a 4.096 byte, sposta i limiti delle risorse nel messaggio stesso e rimuove le ALT.
Solana supporta tre formati di transazione: legacy, v0 e v1. Ciascuno viene descritto di seguito con le stesse tre parti: come dispone i byte sul canale di trasmissione, come referenzia gli account e da dove derivano i limiti delle risorse.
Stato di attivazione di v1
Il formato v1 è attivo su mainnet, devnet e testnet. La lettura delle transazioni è una breaking change: i client che non effettuano l'opt-in falliscono sulle transazioni v1. Le app esistenti dovrebbero consultare Preparazione per v1.
Confronto tra formati
| Limite | legacy | v0 | v1 |
|---|---|---|---|
| Dimensione massima della transazione | 1.232 byte | 1.232 byte | 4.096 byte |
| Indirizzi degli account | ~32, limitato dalla dimensione | 64, tramite lookup table | 64, inline (96 con SIMD-0596) |
| Address lookup table | non supportato | supportato | non supportato |
| Limiti delle risorse | Istruzioni ComputeBudget | Istruzioni ComputeBudget | message config |
Formato legacy
Il formato originale, ancora predefinito nella maggior parte degli strumenti. Non ha alcun prefisso
di versione: il primo byte della transazione è il conteggio compact-u16 dell'array
delle firme, e il primo byte del messaggio è num_required_signatures,
il cui bit più significativo è sempre non impostato.
Layout wire del formato legacy
| Campo | Dimensione | Descrizione |
|---|---|---|
num_signatures | compact-u16 | Numero di firme |
signatures | num_signatures x 64 byte | Firme Ed25519 |
header | 3 byte | MessageHeader — il primo byte ha il bit di versione non impostato |
num_account_keys | compact-u16 | Numero di chiavi account |
account_keys | num_account_keys x 32 byte | Chiavi pubbliche, tutte inline |
recent_blockhash | 32 byte | Specificatore di durata |
num_instructions | compact-u16 | Numero di istruzioni |
instructions | variabile | Ogni istruzione serializzata in modo contiguo |
Ogni array a lunghezza variabile è preceduto da una lunghezza compact-u16: 1 byte per valori da 0 a 127, 2–3 byte per valori più grandi. Per il layout per istruzione e un calcolo della dimensione effettivo, vedere il formato binario delle transazioni.
Account nel formato legacy
Ogni account è scritto come una chiave pubblica completa di 32 byte in account_keys, e
le istruzioni vi fanno riferimento tramite un indice a 1 byte in quell'array. Non è possibile
referenziare un account non esplicitato nella transazione, il che è ciò che
limita una transazione legacy a circa 32 account prima di esaurire i
1.232 byte disponibili.
Limiti delle risorse nel formato legacy
Il limite delle unità di calcolo, il limite della dimensione dei dati degli account caricati, la dimensione dell'heap e la commissione di priorità vengono tutti richiesti includendo istruzioni del programma ComputeBudget nella transazione. Ciascuna occupa uno slot istruzione e 150 unità di calcolo. Ometterle è sicuro: il runtime ricade sui valori predefiniti di 200.000 CU per istruzione (con un massimo di 1,4 M), un limite di dimensione dati di 64 MiB, un heap di 32 KiB e una commissione di priorità di zero.
Formato v0
v0 è un messaggio legacy con due aggiunte: un byte prefisso di versione 0x80 e un
array address_table_lookups aggiunto dopo le istruzioni. Tutto ciò che precede
questi elementi è byte per byte identico al formato legacy.
Layout wire del formato v0
| Campo | Dimensione | Descrizione |
|---|---|---|
num_signatures | compact-u16 | Numero di firme |
signatures | num_signatures x 64 byte | Firme Ed25519 |
0x80 | 1 byte | Byte prefisso di versione — primo byte del messaggio |
header | 3 byte | MessageHeader (uguale al legacy) |
num_account_keys | compact-u16 | Numero di chiavi account statiche |
static_account_keys | num_account_keys x 32 byte | Chiavi che appaiono letteralmente nella transazione |
recent_blockhash | 32 byte | Specificatore di durata |
num_instructions | compact-u16 | Numero di istruzioni |
instructions | variabile | Stesso formato del legacy |
address_table_lookups | compact-u16 + variabile | Riferimenti ALT (vedi sotto) |
Ogni voce della tabella di ricerca degli indirizzi contiene:
| Campo | Dimensione | Descrizione |
|---|---|---|
account_key | 32 byte | La chiave pubblica dell'account ALT |
writable_indexes | compact-u16 + N x 1 byte | Indici nell'ALT per gli account scrivibili |
readonly_indexes | compact-u16 + N x 1 byte | Indici nell'ALT per gli account di sola lettura |
Address lookup table
Un ALT è un account onchain che memorizza fino a 256 chiavi pubbliche. Facendo riferimento a un ALT, una transazione può includere account aggiuntivi utilizzando indici a 1 byte anziché chiavi pubbliche a 32 byte, riducendo significativamente l'overhead per account.
Durante l'esecuzione, prima che inizi l'elaborazione, il validator risolve tutti i riferimenti ALT in chiavi pubbliche complete. Gli indirizzi risolti vengono aggiunti alle chiavi account statiche per formare l'elenco completo delle chiavi account. Gli account risolti tramite ALT seguono lo stesso ordinamento degli account statici: le ricerche scrivibili precedono quelle di sola lettura.
Le tabelle di ricerca degli indirizzi influenzano solo il modo in cui gli account vengono referenziati nella transazione on-wire. Durante l'esecuzione, il runtime risolve tutti gli indici in indirizzi account completi. Gli account risolti tramite ALT possono essere solo scrivibili o di sola lettura (non firmatari); non possono essere firmatari.
Creare e usare una address lookup table
Questo esempio crea una lookup table, aggiunge un indirizzo e usa la tabella per compilare e inviare una transazione v0.
Da una lookup table è possibile caricare solo account non firmatari. Il fee payer e ogni firmatario richiesto restano nell'elenco degli account statici del messaggio. Inoltre, un indirizzo non può essere cercato nello stesso slot in cui è stato aggiunto, quindi l'esempio attende il successivo slot confermato prima di compilare il messaggio v0.
Limiti delle risorse in v0
Invariati rispetto al formato legacy: istruzioni ComputeBudget, con gli stessi valori predefiniti quando vengono omesse.
Formato v1
v1 porta il limite di dimensione a 4.096 byte e ristruttura il messaggio attorno a una transaction config: i limiti delle risorse escono dalle istruzioni ComputeBudget e vengono inseriti in campi a posizione fissa nel messaggio stesso. Questo consente alla rete di classificare una transazione per commissione di priorità con una singola lettura a offset fisso, invece di scansionare e deserializzare l'elenco delle istruzioni.
Layout wire del formato v1
| Campo | Dimensione | Descrizione |
|---|---|---|
0x81 | 1 byte | Byte prefisso di versione — il primo byte della transazione |
header | 3 byte | MessageHeader (uguale al legacy) |
config_mask | 4 byte | Bitmask u32 LE che indica quali valori di configurazione sono presenti |
recent_blockhash | 32 byte | Specificatore di durata |
num_instructions | 1 byte | Conteggio a larghezza fissa, max 64 |
num_addresses | 1 byte | Conteggio a larghezza fissa, max 64 (96 con SIMD-0596) |
addresses | N x 32 byte | Indirizzi degli account, tutti inline — nessun riferimento a lookup table |
config_values | 0–20 byte | Un valore per ogni bit della maschera impostato, nell'ordine dei bit (vedi sotto) |
instruction_headers | N x 4 byte | Per istruzione: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | variabile | Per istruzione: indici degli account, poi instruction data |
signatures | N x 64 byte | In coda, senza prefisso di lunghezza — il conteggio proviene dall'header |
Due differenze strutturali rispetto ai formati legacy e v0 meritano attenzione durante la scrittura di un
decoder. I conteggi sono campi u8 a larghezza fissa anziché compact-u16, e le
istruzioni sono suddivise in due sequenze: prima tutti gli header a dimensione fissa, poi tutti
i payload a lunghezza variabile, invece di ogni istruzione essere contigua.
Account in v1: nessuna address lookup table
v1 rimuove deliberatamente il supporto ALT. 64 indirizzi raw occupano 2.048 byte, e i 96 indirizzi consentiti dopo SIMD-0596 occupano 3.072 byte, entrambi ampiamente entro il limite di 4.096 byte, quindi ogni indirizzo è inline come nel formato legacy. Se la tua applicazione dipende dalle lookup table, passare a v1 significa inserire inline quegli indirizzi.
Limiti delle risorse in v1: la transaction config
La configurazione è una bitmask u32 seguita da valori a larghezza fissa per ogni campo
il cui bit è impostato:
| Bit | Campo | Larghezza | Note |
|---|---|---|---|
| 0–1 | Commissione di priorità | u64 | lamport totali — entrambi i bit impostati insieme |
| 2 | Limite unità di calcolo | u32 | |
| 3 | Limite dimensione dati account caricati | u32 | |
| 4 | Dimensione heap richiesta | u32 |
I bit sconosciuti vengono rifiutati. Poiché il messaggio è firmato, i campi di configurazione non riconosciuti non possono essere eliminati silenziosamente.
La commissione di priorità è in lamport totali, non un prezzo
In legacy e v0, la commissione di priorità viene impostata tramite SetComputeUnitPrice come
micro-lamport per unità di calcolo, moltiplicato per il limite di unità di calcolo. In
v1 è un totale assoluto in lamport — nessuna moltiplicazione, nessun arrotondamento.
Non trasportare l'aritmetica per-CU. La formula della commissione totale rimane
invariata: (signatures × lamports_per_signature) + priority_fee.
Le istruzioni ComputeBudget sono no-op in v1
Una transazione v1 non rifiuta le istruzioni ComputeBudget — le ignora ai fini della configurazione. Vengono comunque eseguite come no-op con esito positivo, consumando 150 unità di calcolo e uno dei 64 slot istruzione senza alcun effetto sul budget. Rimuovile quando costruisci transazioni v1 e smetti di scansionarle quando leggi transazioni v1: i valori risiedono nella message config.
I campi di configurazione devono essere impostati esplicitamente
Il cambiamento comportamentale più importante per i mittenti: a differenza di legacy e v0, le transazioni v1 devono impostare esplicitamente il limite di unità di calcolo e il limite della dimensione dei dati degli account caricati, altrimenti la transazione fallirà.
| Campo non impostato | legacy / v0 | v1 | Sintomo se omesso |
|---|---|---|---|
| Limite unità di calcolo | 200k per istruzione, max 1,4M | 0 CU | Fallisce immediatamente, budget esaurito |
| Dimensione dati account caricati | 64 MiB | 0 byte | MaxLoadedAccountsDataSizeExceeded al primo account caricato |
| Commissione di priorità | 0 | 0 | — |
| Dimensione heap | 32 KiB | 32 KiB | — |
L'approccio consigliato è simulare una volta con entrambi i limiti al massimo, poi scrivere
unitsConsumed e loadedAccountsDataSize restituiti nella configurazione,
arrotondando la dimensione dei dati alla pagina di 32 KiB successiva per margine di sicurezza (il modello di costo del blocco
calcola in pagine da 32 KiB, quindi il margine al di sotto del limite della pagina successiva è
gratuito).
Costruire una transazione v1
Un messaggio v1 viene assemblato attraverso la stessa pipeline di uno legacy o v0. La
differenza è setTransactionMessageConfig: i limiti delle risorse sono una proprietà
del messaggio, quindi l'elenco delle istruzioni contiene solo le istruzioni del programma.
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 espone anche un setter per ogni campo di configurazione —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit e
setTransactionMessageHeapSize — per impostarli uno alla volta.
Dimensionare i limiti tramite simulazione
Invece di tirare a indovinare i due limiti obbligatori, misurali. La simulazione viene eseguita con entrambi i limiti portati al massimo del runtime, quindi non può fallire per mancanza delle risorse che sta misurando. Il valore restituito è il costo esatto di una singola esecuzione simulata, quindi aggiungi un margine prima di fare affidamento su di esso.
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),);
Preparazione per v1
v1 cambia la lettura delle transazioni, non solo il loro invio. Qualsiasi client che
chiama getTransaction o getBlock senza l'opt-in fallisce sulle transazioni v1:
- Passa
maxSupportedTransactionVersion: 1— l'intero JSON1, non la stringa"1"— agetTransactionegetBlock. Passare0fallisce sulle transazioni v1 esattamente come omettere il parametro, quindi una base di codice aggiornata durante il rollout di v0 ha comunque bisogno del valore aggiornato. getTransactionfallisce su una transazione v1 con errore-32015, e una singola transazione v1 fa fallire l'intera rispostagetBlockcon lo stesso errore — non esiste un risultato parziale.blockSubscribeemetteblock: nulle smette di avanzare, quindi un consumer che interpreta questo come un blocco vuoto rimane silenziosamente indietro a partire dal primo slot v1 in avanti.getSignaturesForAddressnon ispeziona mai i corpi delle transazioni, quindi le firme v1 vengono elencate normalmente.- Le risposte con opt-in includono un oggetto
transactionConfignel messaggio per le transazioni v1 (del tutto assente per legacy e v0). Le pipeline che ricavano le priority fee o i limiti di calcolo scansionando le istruzioni ComputeBudget riportano silenziosamente zero per ogni transazione v1. - Usa
encoding: 'base64'quando decodifichi le transazioni lato client e persendTransaction/simulateTransactioncon transazioni superiori a 1.232 byte — la codifica base58 rimane limitata alla vecchia dimensione. - Il supporto delle librerie client richiede versioni recenti:
@solana/kit8.0+, crate Rust della generazione Agave 4.2.x, o web3.js v3. web3.js v1 legge v1 dalla versione 1.99.0 in poi, ma non può costruirlo né inviarlo.
Leggere una transazione v1
Per l'opt-in si usa l'intero JSON 1. Decodificando i byte restituiti si ottengono la
versione e la config, ovvero dove risiedono i limiti delle risorse: scansionare
l'elenco delle istruzioni alla ricerca di istruzioni ComputeBudget non trova nulla.
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);}
Per la guida completa alla migrazione — incluso il supporto delle librerie client, il rilevamento della versione nello streaming (Geyser/gRPC) e il comportamento della simulazione — consulta la pagina di upgrade a Transaction Format v1. Gli esempi eseguibili in TypeScript, Rust, Python e Go sono disponibili in solana-foundation/transaction-v1-examples.
Is this page helpful?