Résumé
Une transaction contient des signatures + un message. Le message contient un en-tête, des adresses de compte, un blockhash récent et des instructions compilées. Taille sérialisée maximale : 1 232 octets.
Une
Transaction
comporte deux champs de niveau supérieur :
signatures: un tableau de signaturesmessage: informations de transaction, incluant la liste des instructions à traiter
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Diagramme montrant les deux parties d'une transaction
La taille sérialisée totale d'une transaction ne doit pas dépasser
PACKET_DATA_SIZE
(1 232 octets). Cette limite équivaut à 1 280 octets (la MTU minimale IPv6)
moins 48 octets pour les en-têtes réseau (40 octets IPv6 + 8 octets d'en-tête de
fragment). Les 1 232 octets incluent à la fois le tableau
signatures et la structure message.
Diagramme montrant le format de transaction et les limites de taille
Signatures
Le champ signatures est un tableau encodé de manière compacte de valeurs
Signature.
Chaque Signature est une signature Ed25519 de 64 octets du Message
sérialisé, signée avec la clé privée du compte signataire. Une signature est
requise pour chaque compte signataire référencé par les
instructions de la transaction.
La première signature du tableau appartient au payeur de frais, le compte qui paie les frais de base et les frais de priorisation de la transaction. Cette première signature sert également d'identifiant de transaction, utilisé pour rechercher la transaction sur le réseau. L'identifiant de transaction est communément appelé la signature de transaction.
Exigences du payeur de frais :
- Doit être le premier compte dans le message (index 0) et un signataire.
- Doit être un compte détenu par le programme système ou un compte nonce (validé
par
validate_fee_payer). - Doit détenir suffisamment de lamports pour couvrir
rent_exempt_minimum + total_fee; sinon la transaction échoue avecInsufficientFundsForFee.
Message
Le champ message est une structure
Message
contenant la charge utile de la transaction :
header: l'en-tête du messageaccount_keys: un tableau d'adresses de comptes requises par les instructions de la transactionrecent_blockhash: un blockhash qui agit comme horodatage pour la transactioninstructions: un tableau d'instructions
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>,}
En-tête
Le champ header est une structure
MessageHeader
avec trois champs u8 qui partitionnent le tableau account_keys en groupes de
permissions :
num_required_signatures: nombre total de signatures requises par la transaction.num_readonly_signed_accounts: nombre de comptes signés en lecture seule.num_readonly_unsigned_accounts: nombre de comptes non signés en lecture seule.
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,}
Diagramme montrant les trois parties de l'en-tête du message
Adresses de comptes
Le champ
account_keys
est un tableau encodé de manière compacte de clés publiques. Chaque entrée
identifie un compte utilisé par au moins une des instructions de la transaction.
Le tableau doit inclure tous les comptes et doit suivre cet ordre strict :
- Signataire + Modifiable
- Signataire + Lecture seule
- Non-signataire + Modifiable
- Non-signataire + Lecture seule
Cet ordre strict permet de combiner le tableau account_keys avec les trois
compteurs dans l'header du message pour déterminer les
permissions de chaque compte sans stocker de drapeaux de métadonnées par
compte. Les compteurs de l'en-tête partitionnent le tableau en quatre groupes
de permissions listés ci-dessus.
Diagramme montrant l'ordre du tableau d'adresses de comptes
Blockhash récent
Le champ recent_blockhash est un hash de 32 octets qui remplit deux fonctions
:
- Horodatage : prouve que la transaction a été créée récemment.
- Déduplication : empêche la même transaction d'être traitée deux fois.
Un blockhash expire après 150 slots. Si le blockhash n'est plus valide lorsque
la transaction arrive, elle est rejetée avec BlockhashNotFound, sauf s'il
s'agit d'une transaction nonce durable
valide.
La méthode RPC getLatestBlockhash vous
permet d'obtenir le blockhash actuel et la dernière hauteur de bloc à laquelle
le blockhash sera valide.
Instructions
Le champ
instructions
est un tableau encodé de manière compacte de structures
CompiledInstruction.
Chaque CompiledInstruction référence les comptes par index dans le tableau
account_keys plutôt que par clé publique complète. Il contient :
program_id_index: index dansaccount_keysidentifiant le programme à invoquer.accounts: tableau d'indices dansaccount_keysspécifiant les comptes à transmettre au programme.data: tableau d'octets contenant le discriminateur d'instruction et les arguments sérialisés.
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>,}
Tableau compact d'instructions
Format binaire de transaction
Les transactions sont sérialisées en utilisant un schéma d'encodage compact. Tous les tableaux de longueur variable (signatures, clés de compte, instructions) sont préfixés par un encodage de longueur compact-u16. Ce format utilise 1 octet pour les valeurs 0-127 et 2-3 octets pour les valeurs plus grandes.
Structure de transaction legacy (sur le réseau) :
| Champ | Taille | Description |
|---|---|---|
num_signatures | 1-3 octets (compact-u16) | Nombre de signatures |
signatures | num_signatures x 64 octets | Signatures Ed25519 |
num_required_signatures | 1 octet | Champ 1 de MessageHeader |
num_readonly_signed | 1 octet | Champ 2 de MessageHeader |
num_readonly_unsigned | 1 octet | Champ 3 de MessageHeader |
num_account_keys | 1-3 octets (compact-u16) | Nombre de clés de compte statiques |
account_keys | num_account_keys x 32 octets | Clés publiques |
recent_blockhash | 32 octets | Blockhash |
num_instructions | 1-3 octets (compact-u16) | Nombre d'instructions |
instructions | variable | Tableau d'instructions compilées |
Chaque instruction compilée est sérialisée comme suit :
| Champ | Taille | Description |
|---|---|---|
program_id_index | 1 octet | Index dans les clés de compte |
num_accounts | 1-3 octets (compact-u16) | Nombre d'indices de compte |
account_indices | num_accounts x 1 octet | Indices de clés de compte |
data_len | 1-3 octets (compact-u16) | Longueur des données d'instruction |
data | data_len octets | Données d'instruction opaques |
Calcul de la taille
Étant donné PACKET_DATA_SIZE = 1 232 octets, l'espace disponible peut être
calculé :
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
Exemple : transaction de transfert de SOL
Le diagramme ci-dessous montre comment les transactions et les instructions fonctionnent ensemble pour permettre aux utilisateurs d'interagir avec le réseau. Dans cet exemple, des SOL sont transférés d'un compte à un autre.
Les métadonnées du compte expéditeur indiquent qu'il doit signer la transaction. Cela permet au System Program de déduire des lamports. Les comptes expéditeur et destinataire doivent être modifiables, afin que leur solde en lamports puisse changer. Pour exécuter cette instruction, le portefeuille de l'expéditeur envoie la transaction contenant sa signature et le message contenant l'instruction de transfert de SOL.
Diagramme de transfert de SOL
Après l'envoi de la transaction, le System Program traite l'instruction de transfert et met à jour le solde en lamports des deux comptes.
Diagramme du processus de transfert de SOL
Vérifiez le destinataire avant d'envoyer des SOL
Un transfert par System Program ajoute des lamports à n'importe quel compte. Il n'existe aucune vérification au niveau du protocole garantissant que le destinataire peut renvoyer les SOL. Les lamports ne peuvent être retirés que par le programme propriétaire du compte. Ainsi, envoyer des SOL à un token mint, à un programme ou à une PDA que vous ne contrôlez pas risque une perte définitive de fonds — seule une autorité désignée par le programme propriétaire peut les restituer. Les SOL envoyés à un token account ne sont récupérables que par le propriétaire de ce compte, jamais par l'expéditeur.
Les transferts de tokens SPL sont partiellement auto-protégés : le Token Program rejette tout transfert dont les comptes ne correspondent pas au mint attendu. Les transferts natifs de SOL ne bénéficient d'aucune telle protection, c'est pourquoi l'expéditeur doit vérifier le destinataire avant de signer. Consultez Vérifier l'adresse pour la logique de classification complète.
L'exemple ci-dessous présente le code correspondant aux diagrammes précédents.
Consultez la fonction
transfer
du 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'exemple suivant illustre la structure d'une transaction contenant une seule instruction de transfert de 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));
Le code ci-dessous affiche la sortie des extraits de code précédents. Le format varie selon les SDK, mais notez que chaque instruction contient les mêmes informations requises.
{"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}}]}
Vérifiez le destinataire avant d'effectuer un transfert
Étant donné qu'un transfert de SOL aboutit vers n'importe quel compte, vérifiez le destinataire avant de signer. Récupérez le compte et n'envoyez des SOL qu'à un portefeuille System Program (ou à une adresse sur la courbe non financée) ; rejetez les mints, les token accounts, les programmes et les PDA que vous ne contrôlez pas.
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);
Cet extrait vérifie les destinataires SOL natifs. Pour la classification complète qui gère également les envois de token SPL (token accounts, ATA, Token-2022), consultez Vérifier une adresse.
Récupération des détails d'une transaction
Après soumission, récupérez les détails de la transaction à l'aide de la signature de transaction et de la méthode RPC getTransaction.
Vous pouvez également retrouver la transaction via 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"}
Is this page helpful?