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 firmemessage: informazioni sulla transazione, inclusa la lista delle istruzioni da elaborare
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Diagramma 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 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 conInsufficientFundsForFee.
Messaggio
Il campo message è una struct
Message
che contiene il payload della transazione:
header: L'intestazione del messaggioaccount_keys: Un array di indirizzi account richiesti dalle istruzioni della transazionerecent_blockhash: Un blockhash che funge da timestamp per la transazioneinstructions: Un array di istruzioni
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.
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 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:
- Firmatario + Scrivibile
- Firmatario + Sola lettura
- Non firmatario + Scrivibile
- 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 account
Blockhash recente
Il campo recent_blockhash è un hash di 32 byte che serve a due scopi:
- Timestamp: dimostra che la transazione è stata creata di recente.
- 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:
program_id_index: Indice nell'arrayaccount_keysche identifica il programma da invocare.accounts: Array di indici inaccount_keysche specifica gli account da passare al programma.data: Array di byte contenente il discriminatore dell'istruzione e gli argomenti serializzati.
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 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):
| Campo | Dimensione | Descrizione |
|---|---|---|
num_signatures | 1-3 byte (compact-u16) | Numero di firme |
signatures | num_signatures x 64 byte | Firme Ed25519 |
num_required_signatures | 1 byte | Campo 1 di MessageHeader |
num_readonly_signed | 1 byte | Campo 2 di MessageHeader |
num_readonly_unsigned | 1 byte | Campo 3 di MessageHeader |
num_account_keys | 1-3 byte (compact-u16) | Numero di chiavi account statiche |
account_keys | num_account_keys x 32 byte | pubkey |
recent_blockhash | 32 byte | Blockhash |
num_instructions | 1-3 byte (compact-u16) | Numero di istruzioni |
instructions | variabile | Array di istruzioni compilate |
Ogni istruzione compilata viene serializzata come:
| Campo | Dimensione | Descrizione |
|---|---|---|
program_id_index | 1 byte | Indice nelle chiavi account |
num_accounts | 1-3 byte (compact-u16) | Numero di indici account |
account_indices | num_accounts x 1 byte | Indici delle chiavi account |
data_len | 1-3 byte (compact-u16) | Lunghezza di instruction data |
data | data_len byte | instruction 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 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 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 transferconst { 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 recipientconst 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 transferconst { 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);
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 transferconst 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 recipientconst transferInstruction = client.system.instructions.transferSol({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
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.
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);
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.
{"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?