Transazioni con versione

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

Limitelegacyv0v1
Dimensione massima della transazione1.232 byte1.232 byte4.096 byte
Indirizzi degli account~32, limitato dalla dimensione64, tramite lookup table64, inline (96 con SIMD-0596)
Address lookup tablenon supportatosupportatonon supportato
Limiti delle risorseIstruzioni ComputeBudgetIstruzioni ComputeBudgetmessage config

Vai a: Legacy · v0 · v1

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

CampoDimensioneDescrizione
num_signaturescompact-u16Numero di firme
signaturesnum_signatures x 64 byteFirme Ed25519
header3 byteMessageHeader — il primo byte ha il bit di versione non impostato
num_account_keyscompact-u16Numero di chiavi account
account_keysnum_account_keys x 32 byteChiavi pubbliche, tutte inline
recent_blockhash32 byteSpecificatore di durata
num_instructionscompact-u16Numero di istruzioni
instructionsvariabileOgni 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

CampoDimensioneDescrizione
num_signaturescompact-u16Numero di firme
signaturesnum_signatures x 64 byteFirme Ed25519
0x801 byteByte prefisso di versione — primo byte del messaggio
header3 byteMessageHeader (uguale al legacy)
num_account_keyscompact-u16Numero di chiavi account statiche
static_account_keysnum_account_keys x 32 byteChiavi che appaiono letteralmente nella transazione
recent_blockhash32 byteSpecificatore di durata
num_instructionscompact-u16Numero di istruzioni
instructionsvariabileStesso formato del legacy
address_table_lookupscompact-u16 + variabileRiferimenti ALT (vedi sotto)

Ogni voce della tabella di ricerca degli indirizzi contiene:

CampoDimensioneDescrizione
account_key32 byteLa chiave pubblica dell'account ALT
writable_indexescompact-u16 + N x 1 byteIndici nell'ALT per gli account scrivibili
readonly_indexescompact-u16 + N x 1 byteIndici 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

CampoDimensioneDescrizione
0x811 byteByte prefisso di versione — il primo byte della transazione
header3 byteMessageHeader (uguale al legacy)
config_mask4 byteBitmask u32 LE che indica quali valori di configurazione sono presenti
recent_blockhash32 byteSpecificatore di durata
num_instructions1 byteConteggio a larghezza fissa, max 64
num_addresses1 byteConteggio a larghezza fissa, max 64 (96 con SIMD-0596)
addressesN x 32 byteIndirizzi degli account, tutti inline — nessun riferimento a lookup table
config_values0–20 byteUn valore per ogni bit della maschera impostato, nell'ordine dei bit (vedi sotto)
instruction_headersN x 4 bytePer istruzione: program_id_index (u8), num_accounts (u8), data_len (u16 LE)
instruction_payloadsvariabilePer istruzione: indici degli account, poi instruction data
signaturesN x 64 byteIn 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:

BitCampoLarghezzaNote
0–1Commissione di prioritàu64lamport totali — entrambi i bit impostati insieme
2Limite unità di calcolou32
3Limite dimensione dati account caricatiu32
4Dimensione heap richiestau32

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 impostatolegacy / v0v1Sintomo se omesso
Limite unità di calcolo200k per istruzione, max 1,4M0 CUFallisce immediatamente, budget esaurito
Dimensione dati account caricati64 MiB0 byteMaxLoadedAccountsDataSizeExceeded al primo account caricato
Commissione di priorità00—
Dimensione heap32 KiB32 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.

Kit
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);
Console
Click to execute the code.

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.

Kit
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),
);
Console
Click to execute the code.

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 JSON 1, non la stringa "1" — a getTransaction e getBlock. Passare 0 fallisce 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.
  • getTransaction fallisce su una transazione v1 con errore -32015, e una singola transazione v1 fa fallire l'intera risposta getBlock con lo stesso errore — non esiste un risultato parziale.
  • blockSubscribe emette block: null e smette di avanzare, quindi un consumer che interpreta questo come un blocco vuoto rimane silenziosamente indietro a partire dal primo slot v1 in avanti.
  • getSignaturesForAddress non ispeziona mai i corpi delle transazioni, quindi le firme v1 vengono elencate normalmente.
  • Le risposte con opt-in includono un oggetto transactionConfig nel 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 per sendTransaction/simulateTransaction con transazioni superiori a 1.232 byte — la codifica base58 rimane limitata alla vecchia dimensione.
  • Il supporto delle librerie client richiede versioni recenti: @solana/kit 8.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.

Kit
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);
}
Console
Click to execute the code.

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?

© 2026 Solana Foundation. Tutti i diritti riservati.