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
| Limite | legacy | v0 | v1 |
|---|---|---|---|
| Tamanho máximo da transação | 1.232 bytes | 1.232 bytes | 4.096 bytes |
| Endereços de contas | ~32, limitado pelo tamanho | 64, via tabelas de lookup | 64, inline (96 com SIMD-0596) |
| Address lookup tables | não suportado | suportado | não suportado |
| Limites de recursos | Instruções ComputeBudget | Instruções ComputeBudget | configuração da mensagem |
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
| Campo | Tamanho | Descrição |
|---|---|---|
num_signatures | compact-u16 | Número de assinaturas |
signatures | num_signatures x 64 bytes | Assinaturas Ed25519 |
header | 3 bytes | MessageHeader — o primeiro byte tem o bit de versão desativado |
num_account_keys | compact-u16 | Número de chaves de conta |
account_keys | num_account_keys x 32 bytes | Chaves públicas, todas inline |
recent_blockhash | 32 bytes | Especificador de tempo de vida |
num_instructions | compact-u16 | Número de instruções |
instructions | variável | Cada 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
| Campo | Tamanho | Descrição |
|---|---|---|
num_signatures | compact-u16 | Número de assinaturas |
signatures | num_signatures x 64 bytes | Assinaturas Ed25519 |
0x80 | 1 byte | Byte de prefixo de versão — primeiro byte da mensagem |
header | 3 bytes | MessageHeader (igual ao legado) |
num_account_keys | compact-u16 | Número de chaves de conta estáticas |
static_account_keys | num_account_keys x 32 bytes | Chaves que aparecem literalmente na transação |
recent_blockhash | 32 bytes | Especificador de tempo de vida |
num_instructions | compact-u16 | Número de instruções |
instructions | variável | Mesmo formato que o legado |
address_table_lookups | compact-u16 + variável | Referências ALT (ver abaixo) |
Cada entrada da tabela de pesquisa de endereços contém:
| Campo | Tamanho | Descrição |
|---|---|---|
account_key | 32 bytes | A chave pública da conta ALT |
writable_indexes | compact-u16 + N x 1 byte | Índices na ALT para contas graváveis |
readonly_indexes | compact-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
| Campo | Tamanho | Descrição |
|---|---|---|
0x81 | 1 byte | Byte de prefixo de versão — o primeiro byte da transação |
header | 3 bytes | MessageHeader (igual ao legado) |
config_mask | 4 bytes | Bitmask u32 LE indicando quais valores de configuração estão presentes |
recent_blockhash | 32 bytes | Especificador de tempo de vida |
num_instructions | 1 byte | Contagem de largura fixa, máx. 64 |
num_addresses | 1 byte | Contagem de largura fixa, máx. 64 (96 com SIMD-0596) |
addresses | N x 32 bytes | Endereços de contas, todos inline — sem referências a tabelas de lookup |
config_values | 0–20 bytes | Um valor por bit de máscara definido, em ordem de bits (ver abaixo) |
instruction_headers | N x 4 bytes | Por instrução: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | variável | Por instrução: índices de contas, depois instruction data |
signatures | N x 64 bytes | No 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) | Campo | Largura | Notas |
|---|---|---|---|
| 0–1 | Taxa de prioridade | u64 | Total em lamports — ambos os bits definidos juntos |
| 2 | Limite de unidades de computação | u32 | |
| 3 | Limite de tamanho dos dados de contas carregadas | u32 | |
| 4 | Tamanho de heap solicitado | u32 |
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 definido | legacy / v0 | v1 | Sintoma se omitido |
|---|---|---|---|
| Limite de unidades de computação | 200k por instrução, máx. 1,4M | 0 CU | Falha imediatamente, sem orçamento |
| Tamanho dos dados de contas carregadas | 64 MiB | 0 bytes | MaxLoadedAccountsDataSizeExceeded na primeira conta carregada |
| Taxa de prioridade | 0 | 0 | — |
| Tamanho do heap | 32 KiB | 32 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.
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);
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.
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),);
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 JSON1, não a string"1"— paragetTransactionegetBlock. Passar0falha 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. getTransactionfalha em uma transação v1 com o erro-32015, e uma transação v1 falha em toda a resposta degetBlockcom o mesmo erro — não há resultado parcial.blockSubscribeemiteblock: nulle 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.getSignaturesForAddressnunca inspeciona os corpos das transações, portanto assinaturas v1 são listadas normalmente.- As respostas com adesão incluem um objeto
transactionConfigna 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 parasendTransaction/simulateTransactioncom 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/kit8.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.
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);}
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?