Transações versionadas

Resumo

O Solana possui três formatos de transação: legacy, v0 e v1. O v0 adiciona Address Lookup Tables (ALTs) para referenciar contas por índices de 1 byte. O v1 aumenta o limite de tamanho para 4.096 bytes, move os limites de recursos para a própria mensagem e remove as ALTs.

O Solana suporta três formatos de transação: legacy, v0 e v1. Cada um é descrito abaixo com as mesmas três partes: como organiza seus bytes na rede, como referencia contas e de onde vêm seus limites de recursos.

Status de ativação do v1

O formato v1 está ativo na mainnet, devnet e testnet. A leitura de transações é uma mudança incompatível: clientes que não aderirem falham em transações v1. Os apps existentes devem consultar Preparando-se para o v1.

Comparação de formatos

Limitelegacyv0v1
Tamanho máximo da transação1.232 bytes1.232 bytes4.096 bytes
Endereços de contas~32, limitado pelo tamanho64, via tabelas de lookup64, inline (96 com SIMD-0596)
Address lookup tablesnão suportadosuportadonão suportado
Limites de recursosInstruções ComputeBudgetInstruções ComputeBudgetconfiguração da mensagem

Ir para: Legacy · v0 · v1

Formato legacy

O formato original, e ainda o padrão na maioria das ferramentas. Não possui nenhum prefixo de versão: o primeiro byte da transação é a contagem compact-u16 do array de assinaturas, e o primeiro byte da mensagem é num_required_signatures, cujo bit mais significativo está sempre desativado.

Layout de bytes do formato legacy

CampoTamanhoDescrição
num_signaturescompact-u16Número de assinaturas
signaturesnum_signatures x 64 bytesAssinaturas Ed25519
header3 bytesMessageHeader — o primeiro byte tem o bit de versão desativado
num_account_keyscompact-u16Número de chaves de conta
account_keysnum_account_keys x 32 bytesChaves públicas, todas inline
recent_blockhash32 bytesEspecificador de tempo de vida
num_instructionscompact-u16Número de instruções
instructionsvariávelCada instrução serializada de forma contígua

Todo array de comprimento variável é prefixado com um comprimento compact-u16: 1 byte para valores de 0 a 127, 2 a 3 bytes para valores maiores. Para o layout por instrução e um cálculo de tamanho detalhado, consulte formato binário de transação.

Contas no formato legacy

Cada conta é escrita como uma chave pública completa de 32 bytes em account_keys, e as instruções as referenciam por um índice de 1 byte nesse array. Não há como referenciar uma conta que não esteja explicitada na transação, o que limita uma transação legacy a aproximadamente 32 contas antes de esgotar seus 1.232 bytes.

Limites de recursos no formato legacy

O limite de unidades de computação, o limite de tamanho dos dados de contas carregadas, o tamanho do heap e a taxa de prioridade são todos solicitados incluindo instruções do programa ComputeBudget na transação. Cada uma consome um slot de instrução e 150 unidades de computação. Omiti-las é seguro: o runtime utiliza os padrões de 200.000 CU por instrução (limitado a 1,4M), um limite de tamanho de dados de 64 MiB, um heap de 32 KiB e uma taxa de prioridade zero.

Formato v0

O v0 é uma mensagem legacy acrescida de dois elementos: um byte de prefixo de versão 0x80 e um array address_table_lookups anexado após as instruções. Tudo antes desses elementos é byte a byte idêntico ao legacy.

Layout de bytes do formato v0

CampoTamanhoDescrição
num_signaturescompact-u16Número de assinaturas
signaturesnum_signatures x 64 bytesAssinaturas Ed25519
0x801 byteByte de prefixo de versão — primeiro byte da mensagem
header3 bytesMessageHeader (igual ao legado)
num_account_keyscompact-u16Número de chaves de conta estáticas
static_account_keysnum_account_keys x 32 bytesChaves que aparecem literalmente na transação
recent_blockhash32 bytesEspecificador de tempo de vida
num_instructionscompact-u16Número de instruções
instructionsvariávelMesmo formato que o legado
address_table_lookupscompact-u16 + variávelReferências ALT (ver abaixo)

Cada entrada da tabela de pesquisa de endereços contém:

CampoTamanhoDescrição
account_key32 bytesA chave pública da conta ALT
writable_indexescompact-u16 + N x 1 byteÍndices na ALT para contas graváveis
readonly_indexescompact-u16 + N x 1 byteÍndices na ALT para contas somente leitura

Address lookup tables

Uma ALT é uma conta on-chain que armazena até 256 chaves públicas. Ao referenciar uma ALT, uma transação pode incluir contas adicionais usando índices de 1 byte em vez de chaves públicas de 32 bytes, reduzindo significativamente a sobrecarga por conta.

Em tempo de execução, antes do início da execução, o validator resolve todas as referências ALT em chaves públicas completas. Os endereços resolvidos são anexados às chaves de conta estáticas para formar a lista completa de chaves de conta. As contas resolvidas por ALT seguem a mesma ordenação das contas estáticas: pesquisas graváveis vêm antes das pesquisas somente leitura.

As tabelas de pesquisa de endereços afetam apenas como as contas são referenciadas na transação transmitida. Em tempo de execução, o runtime resolve todos os índices para endereços de conta completos. As contas resolvidas por ALT podem ser apenas graváveis ou somente leitura (não signatárias); elas não podem ser signatárias.

Criar e usar uma tabela de consulta de endereços

Este exemplo cria uma tabela de consulta, adiciona um endereço e usa a tabela para compilar e enviar uma transação v0.

Somente contas que não são assinantes podem ser carregadas de uma tabela de consulta. O pagador de taxas e todos os assinantes necessários permanecem na lista de contas estáticas da mensagem. Além disso, um endereço não pode ser consultado no mesmo slot em que foi adicionado, então o exemplo aguarda o próximo slot confirmado antes de compilar a mensagem v0.

Limites de recursos no v0

Sem alterações em relação ao legacy: instruções ComputeBudget, com os mesmos valores padrão quando omitidas.

Formato v1

O v1 aumenta o limite de tamanho para 4.096 bytes e reestrutura a mensagem em torno de uma configuração de transação: os limites de recursos saem das instruções ComputeBudget e passam para campos de posição fixa na própria mensagem. Isso permite que a rede classifique uma transação por taxa de prioridade com uma única leitura de offset fixo, em vez de varrer e desserializar sua lista de instruções.

Layout de bytes do formato v1

CampoTamanhoDescrição
0x811 byteByte de prefixo de versão — o primeiro byte da transação
header3 bytesMessageHeader (igual ao legado)
config_mask4 bytesBitmask u32 LE indicando quais valores de configuração estão presentes
recent_blockhash32 bytesEspecificador de tempo de vida
num_instructions1 byteContagem de largura fixa, máx. 64
num_addresses1 byteContagem de largura fixa, máx. 64 (96 com SIMD-0596)
addressesN x 32 bytesEndereços de contas, todos inline — sem referências a tabelas de lookup
config_values0–20 bytesUm valor por bit de máscara definido, em ordem de bits (ver abaixo)
instruction_headersN x 4 bytesPor instrução: program_id_index (u8), num_accounts (u8), data_len (u16 LE)
instruction_payloadsvariávelPor instrução: índices de contas, depois instruction data
signaturesN x 64 bytesNo final, sem prefixo de comprimento — a contagem vem do header

Duas diferenças estruturais em relação ao legacy e ao v0 merecem atenção ao escrever um decodificador. As contagens são campos u8 de largura fixa em vez de compact-u16, e as instruções são divididas em duas sequências: primeiro todos os headers de tamanho fixo, depois todos os payloads de comprimento variável, em vez de cada instrução ser contígua.

Contas no v1: sem address lookup tables

O v1 remove o suporte a ALT deliberadamente. 64 endereços brutos equivalem a 2.048 bytes, e os 96 endereços permitidos após a SIMD-0596 equivalem a 3.072 bytes, ambos confortavelmente dentro do limite de 4.096 bytes, então todo endereço fica inline, como no formato legacy. Se seu aplicativo depende de tabelas de consulta, migrar para o v1 significa incorporar esses endereços inline.

Limites de recursos no v1: a configuração de transação

A configuração é uma bitmask u32 seguida de valores de largura fixa para cada campo cujo bit está definido:

Bit(s)CampoLarguraNotas
0–1Taxa de prioridadeu64Total em lamports — ambos os bits definidos juntos
2Limite de unidades de computaçãou32
3Limite de tamanho dos dados de contas carregadasu32
4Tamanho de heap solicitadou32

Bits desconhecidos são rejeitados. Como a mensagem é assinada, campos de configuração não reconhecidos não podem ser silenciosamente descartados.

A taxa de prioridade é o total em lamports, não um preço

No legacy e no v0, a taxa de prioridade é definida via SetComputeUnitPrice como micro-lamports por unidade de computação, multiplicada pelo limite de unidades de computação. No v1, é um total absoluto em lamports — sem multiplicação, sem arredondamento. Não aplique a aritmética por CU aqui. A fórmula total da taxa permanece inalterada: (signatures × lamports_per_signature) + priority_fee.

Instruções ComputeBudget são no-ops no v1

Uma transação v1 não rejeita instruções ComputeBudget — ela as ignora para fins de configuração. Elas ainda são executadas como no-ops bem-sucedidos, consumindo 150 unidades de computação e um dos 64 slots de instrução sem qualquer efeito no orçamento. Remova-as ao construir transações v1, e pare de buscá-las ao ler transações v1: os valores estão na configuração da mensagem.

Os campos de configuração devem ser definidos explicitamente

A mudança comportamental mais importante para remetentes: ao contrário do legacy e do v0, as transações v1 devem definir explicitamente o limite de unidades de computação e o limite de tamanho dos dados de contas carregadas, caso contrário a transação falhará.

Campo não definidolegacy / v0v1Sintoma se omitido
Limite de unidades de computação200k por instrução, máx. 1,4M0 CUFalha imediatamente, sem orçamento
Tamanho dos dados de contas carregadas64 MiB0 bytesMaxLoadedAccountsDataSizeExceeded na primeira conta carregada
Taxa de prioridade00—
Tamanho do heap32 KiB32 KiB—

A abordagem recomendada é simular uma vez com ambos os limites no máximo, depois gravar os valores retornados de unitsConsumed e loadedAccountsDataSize de volta na configuração, arredondando o tamanho dos dados para cima até a próxima página de 32 KiB para folga (o modelo de custo do bloco cobra em páginas de 32 KiB, portanto a folga abaixo do próximo limite de página é gratuita).

Construindo uma transação v1

Uma mensagem v1 é montada pelo mesmo pipeline de uma mensagem legacy ou v0. A diferença é setTransactionMessageConfig: os limites de recursos são uma propriedade da mensagem, então a lista de instruções contém apenas as instruções do programa.

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.

O Kit também expõe um setter para cada campo de configuração — setTransactionMessagePriorityFeeLamports, setTransactionMessageComputeUnitLimit, setTransactionMessageLoadedAccountsDataSizeLimit e setTransactionMessageHeapSize — para defini-los um de cada vez.

Dimensionando os limites por simulação

Em vez de adivinhar os dois limites obrigatórios, meça-os. A simulação é executada com ambos os limites elevados ao máximo do runtime, então não pode falhar por falta dos recursos que está medindo. O valor retornado é o custo exato de uma execução simulada, portanto adicione uma margem antes de confiar nele.

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.

Preparando-se para o v1

O v1 muda a leitura de transações, não apenas o envio. Qualquer cliente que chame getTransaction ou getBlock sem aderir falha em transações v1:

  • Passe maxSupportedTransactionVersion: 1 — o inteiro JSON 1, não a string "1" — para getTransaction e getBlock. Passar 0 falha em transações v1 exatamente como omitir o parâmetro, portanto uma base de código atualizada durante o lançamento do v0 ainda precisa ter o valor alterado.
  • getTransaction falha em uma transação v1 com o erro -32015, e uma transação v1 falha em toda a resposta de getBlock com o mesmo erro — não há resultado parcial.
  • blockSubscribe emite block: null e para de avançar, portanto um consumidor que interprete isso como um bloco vazio ficará silenciosamente para trás a partir do primeiro slot v1 em diante.
  • getSignaturesForAddress nunca inspeciona os corpos das transações, portanto assinaturas v1 são listadas normalmente.
  • As respostas com adesão incluem um objeto transactionConfig na mensagem para transações v1 (totalmente ausente em legacy e v0). Pipelines que derivam taxas de prioridade ou limites de computação buscando instruções ComputeBudget informam silenciosamente zero para toda transação v1.
  • Use encoding: 'base64' ao decodificar transações no lado do cliente, e para sendTransaction/simulateTransaction com transações acima de 1.232 bytes — a codificação base58 permanece limitada ao tamanho antigo.
  • O suporte em bibliotecas de cliente requer versões recentes: @solana/kit 8.0+, crates Rust da geração Agave 4.2.x, ou web3.js v3. O web3.js v1 lê v1 a partir da versão 1.99.0 em diante, mas não consegue construir nem enviar transações v1.

Lendo uma transação v1

Aderir exige o inteiro JSON 1. Decodificar os bytes retornados fornece a versão e a configuração, que é onde ficam os limites de recursos — buscar instruções ComputeBudget na lista de instruções não encontra nada.

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.

Para o guia de migração completo — incluindo suporte a bibliotecas de cliente, detecção de versão em streaming (Geyser/gRPC) e comportamento de simulação — consulte a página de atualização do Transaction Format v1. Exemplos executáveis em TypeScript, Rust, Python e Go estão em solana-foundation/transaction-v1-examples.

Is this page helpful?

© 2026 Fundação Solana. Todos os direitos reservados.