Struttura delle transazioni

Riepilogo

Una transazione contiene firme + un messaggio. Il messaggio contiene un'intestazione, indirizzi degli account, blockhash recente e istruzioni compilate. Dimensione massima serializzata: 1.232 byte.

Una Transaction ha due campi di primo livello:

  • signatures: un array di firme
  • message: informazioni sulla transazione, inclusa la lista delle istruzioni da elaborare
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Diagramma che mostra le due parti di una transazioneDiagramma che mostra le due parti di una transazione

La dimensione totale serializzata di una transazione non deve superare PACKET_DATA_SIZE (1.232 byte). Questo limite equivale a 1.280 byte (MTU minimo IPv6) meno 48 byte per le intestazioni di rete (40 byte IPv6 + 8 byte intestazione frammento). I 1.232 byte includono sia l'array signatures che lo struct message.

Diagramma che mostra il formato della transazione e i limiti di dimensioneDiagramma che mostra il formato della transazione e i limiti di dimensione

Firme

Il campo signatures è un array con codifica compatta di valori Signature. Ogni Signature è una firma Ed25519 da 64 byte del Message serializzato, firmata con la chiave privata dell'account firmatario. È richiesta una firma per ogni account firmatario referenziato dalle istruzioni della transazione.

Ogni firma è prodotta da una chiave privata. La posizione di tale chiave — un keypair locale, un HSM o KMS cloud, oppure un servizio di wallet gestito — è una decisione di progettazione per la produzione. Vedere Firma in Produzione.

La prima firma nell'array appartiene al fee payer, l'account che paga la commissione base e la commissione di priorità della transazione. Questa prima firma funge anche da ID transazione, utilizzato per individuare la transazione sulla rete. L'ID transazione è comunemente denominato firma della transazione.

Requisiti del fee payer:

  • Deve essere il primo account nel messaggio (indice 0) ed essere un firmatario.
  • Deve essere un account di proprietà del System Program o un account nonce (validato da validate_fee_payer).
  • Deve contenere abbastanza lamport per coprire rent_exempt_minimum + total_fee; altrimenti la transazione fallisce con InsufficientFundsForFee.

Messaggio

Il campo message è una struct Message che contiene il payload della transazione:

Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

Intestazione

Il campo header è una struct MessageHeader con tre campi u8 che suddividono l'array account_keys in gruppi di autorizzazione:

  • num_required_signatures: Numero totale di firme richieste dalla transazione.
  • num_readonly_signed_accounts: Numero di account firmati in sola lettura.
  • num_readonly_unsigned_accounts: Numero di account non firmati in sola lettura.
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

Prefissi dei messaggi legacy e versionati

In un messaggio di transazione legacy, il primo byte del messaggio è num_required_signatures, seguito dagli altri due byte MessageHeader. In un messaggio di transazione versionato, il primo byte è invece un prefisso di versione; i tre byte MessageHeader iniziano immediatamente dopo quel prefisso. Vedi transazioni versionate per il layout completo del messaggio v0.

Diagramma che mostra le tre parti dell'intestazione del messaggioDiagramma che mostra le tre parti dell'intestazione del messaggio

Indirizzi degli account

Il campo account_keys è un array codificato in formato compatto di chiavi pubbliche. Ogni voce identifica un account utilizzato da almeno una delle istruzioni della transazione. L'array deve includere ogni account e deve rispettare questo ordine rigoroso:

  1. Firmatario + Scrivibile
  2. Firmatario + Sola lettura
  3. Non firmatario + Scrivibile
  4. Non firmatario + Sola lettura

Questo ordine rigoroso consente di combinare l'array account_keys con i tre contatori nell'header del messaggio per determinare i permessi di ciascun account senza memorizzare flag di metadati per singolo account. I contatori dell'intestazione suddividono l'array nei quattro gruppi di permessi elencati sopra.

Diagramma che mostra l'ordine dell'array degli indirizzi degli accountDiagramma che mostra l'ordine dell'array degli indirizzi degli account

Blockhash recente

Il campo recent_blockhash è un hash di 32 byte che serve a due scopi:

  1. Timestamp: dimostra che la transazione è stata creata di recente.
  2. Deduplicazione: impedisce che la stessa transazione venga elaborata due volte.

Un blockhash scade dopo 150 slot. Se il blockhash non è più valido quando la transazione arriva, viene rifiutata con BlockhashNotFound, a meno che non sia una valida transazione con nonce durevole.

Il metodo RPC getLatestBlockhash consente di ottenere il blockhash corrente e l'ultima altezza del blocco alla quale il blockhash sarà valido.

Istruzioni

Il campo instructions è un array con codifica compatta di struct CompiledInstruction. Ogni CompiledInstruction fa riferimento agli account tramite indice nell'array account_keys anziché tramite chiave pubblica completa. Contiene:

  1. program_id_index: Indice nell'array account_keys che identifica il programma da invocare.
  2. accounts: Array di indici in account_keys che specifica gli account da passare al programma.
  3. data: Array di byte contenente il discriminatore dell'istruzione e gli argomenti serializzati.
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

Array compatto di istruzioniArray compatto di istruzioni

Formato binario delle transazioni

Le transazioni vengono serializzate utilizzando uno schema di codifica compatta. Tutti gli array a lunghezza variabile (firme, chiavi degli account, istruzioni) sono preceduti da una codifica della lunghezza compact-u16. Questo formato utilizza 1 byte per i valori da 0 a 127 e 2-3 byte per i valori più grandi.

Layout della transazione legacy (sul wire):

CampoDimensioneDescrizione
num_signatures1-3 byte (compact-u16)Numero di firme
signaturesnum_signatures x 64 byteFirme Ed25519
num_required_signatures1 byteCampo 1 di MessageHeader
num_readonly_signed1 byteCampo 2 di MessageHeader
num_readonly_unsigned1 byteCampo 3 di MessageHeader
num_account_keys1-3 byte (compact-u16)Numero di chiavi account statiche
account_keysnum_account_keys x 32 bytepubkey
recent_blockhash32 byteBlockhash
num_instructions1-3 byte (compact-u16)Numero di istruzioni
instructionsvariabileArray di istruzioni compilate

Ogni istruzione compilata viene serializzata come:

CampoDimensioneDescrizione
program_id_index1 byteIndice nelle chiavi account
num_accounts1-3 byte (compact-u16)Numero di indici account
account_indicesnum_accounts x 1 byteIndici delle chiavi account
data_len1-3 byte (compact-u16)Lunghezza di instruction data
datadata_len byteinstruction data opaca

Calcolo della dimensione

Dato PACKET_DATA_SIZE = 1.232 byte, lo spazio disponibile può essere calcolato:

Total = 1232 bytes
- compact-u16(num_sigs) # 1 byte
- num_sigs * 64 # signature bytes
- 3 # message header
- compact-u16(num_keys) # 1 byte
- num_keys * 32 # account key bytes
- 32 # recent blockhash
- compact-u16(num_ixs) # 1 byte
- sum(instruction_sizes) # per-instruction overhead + data

Esempio: transazione di trasferimento SOL

Il diagramma seguente mostra come le transazioni e le istruzioni collaborano per consentire agli utenti di interagire con la rete. In questo esempio, SOL viene trasferito da un account a un altro.

I metadati dell'account mittente indicano che deve firmare la transazione. Ciò consente al System Program di detrarre lamport. Sia l'account mittente che quello destinatario devono essere scrivibili, affinché il loro saldo in lamport possa cambiare. Per eseguire questa istruzione, il wallet del mittente invia la transazione contenente la propria firma e il messaggio con l'istruzione di trasferimento SOL.

Diagramma di trasferimento SOLDiagramma di trasferimento SOL

Dopo l'invio della transazione, il System Program elabora l'istruzione di trasferimento e aggiorna il saldo in lamport di entrambi gli account.

Diagramma del processo di trasferimento SOLDiagramma del processo di trasferimento SOL

Verifica il destinatario prima di inviare SOL

Un trasferimento del System Program aggiunge lamport a qualsiasi account. Non esiste alcun controllo a livello di protocollo che verifichi se il destinatario può spostare nuovamente il SOL. I lamport possono essere spostati solo dal programma proprietario dell'account, quindi inviare SOL a un token mint, a un programma o a un PDA che non controlli rischia la perdita permanente dei fondi — solo un'autorità specificata dal programma proprietario può restituirli. Il SOL inviato a un token account è recuperabile solo dal proprietario di quell'account, mai dal mittente.

I trasferimenti di token SPL sono parzialmente autoprotetti: il Token Program rifiuta un trasferimento i cui account non corrispondono al mint previsto. I trasferimenti nativi di SOL non dispongono di tale protezione, quindi il mittente deve verificare il destinatario prima di firmare. Consulta Verifica indirizzo per la logica di classificazione completa.

L'esempio seguente mostra il codice relativo ai diagrammi precedenti. Consulta la funzione transfer del System Program.

import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const sender = client.payer;
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Check balance before transfer
const { value: preBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: preBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
const transactionSignature = await client.sendTransaction([
transferInstruction
]);
// Check balance after transfer
const { value: postBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: postBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature.context.signature);
Console
Click to execute the code.

L'esempio seguente mostra la struttura di una transazione contenente una singola istruzione di trasferimento SOL.

import {
createClient,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
const sender = client.payer;
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

Il codice seguente mostra l'output dei frammenti di codice precedenti. Il formato varia a seconda degli SDK, ma si noti che ogni istruzione contiene le stesse informazioni richieste.

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

Verifica il destinatario prima di trasferire

Poiché un trasferimento SOL va a buon fine su qualsiasi account, verifica il destinatario prima di firmare. Recupera l'account e invia solo a un wallet System Program (o a un indirizzo sulla curva non finanziato); rifiuta mint, token account, programmi e PDA che non controlli.

Kit
import {
type Address,
createSolanaRpc,
fetchJsonParsedAccount,
isOffCurveAddress
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;
/**
* Throws if `recipient` cannot safely receive native SOL.
*
* Only System Program wallets (or unfunded on-curve addresses) are safe. Any
* other account locks the lamports because no authority can debit them.
*/
async function assertSafeSolRecipient(recipient: Address): Promise<void> {
const account = await fetchJsonParsedAccount(rpc, recipient);
if (!account.exists) {
// Off-curve = a PDA with no account; reject conservatively.
if (isOffCurveAddress(recipient)) {
throw new Error(
"Recipient is a PDA with no account; SOL would be locked"
);
}
// On-curve = an unfunded wallet, safe to fund.
return;
}
if (account.programAddress !== SYSTEM_PROGRAM) {
throw new Error(
`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`
);
}
}
// A wallet: safe.
await assertSafeSolRecipient(
"H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address
);
// The USDC mint: rejected before any SOL leaves the sender.
await assertSafeSolRecipient(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address
);
Console
Click to execute the code.

Questo snippet verifica i destinatari SOL nativi. Per la classificazione completa che gestisce anche gli invii di token SPL (token account, ATA, Token-2022), consulta Verify Address.

Recupero dei dettagli della transazione

Dopo l'invio, recupera i dettagli della transazione utilizzando la firma della transazione e il metodo RPC getTransaction.

Puoi anche trovare la transazione utilizzando Solana Explorer.

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

La risposta grezza identifica gli account tramite indice e memorizza le istruzioni interne (CPI) come blob codificati. Per risolverle in indirizzi e navigare l'intero albero delle istruzioni, consulta Transaction Introspection.

Is this page helpful?

Indice dei contenuti

Modifica pagina
© 2026 Solana Foundation. Tutti i diritti riservati.