Structure des transactions

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 signatures
  • message : informations de transaction, incluant la liste des instructions à traiter
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Diagramme montrant les deux parties d'une transactionDiagramme 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 tailleDiagramme 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 avec InsufficientFundsForFee.

Message

Le champ message est une structure Message contenant la charge utile de la transaction :

  • header : l'en-tête du message
  • account_keys : un tableau d'adresses de comptes requises par les instructions de la transaction
  • recent_blockhash : un blockhash qui agit comme horodatage pour la transaction
  • instructions : un tableau d'instructions
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>,
}

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.
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,
}

Diagramme montrant les trois parties de l'en-tête du messageDiagramme 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 :

  1. Signataire + Modifiable
  2. Signataire + Lecture seule
  3. Non-signataire + Modifiable
  4. 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 comptesDiagramme 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 :

  1. Horodatage : prouve que la transaction a été créée récemment.
  2. 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 :

  1. program_id_index : index dans account_keys identifiant le programme à invoquer.
  2. accounts : tableau d'indices dans account_keys spécifiant les comptes à transmettre au programme.
  3. data : tableau d'octets contenant le discriminateur d'instruction et les arguments sérialisés.
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>,
}

Tableau compact d'instructionsTableau 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) :

ChampTailleDescription
num_signatures1-3 octets (compact-u16)Nombre de signatures
signaturesnum_signatures x 64 octetsSignatures Ed25519
num_required_signatures1 octetChamp 1 de MessageHeader
num_readonly_signed1 octetChamp 2 de MessageHeader
num_readonly_unsigned1 octetChamp 3 de MessageHeader
num_account_keys1-3 octets (compact-u16)Nombre de clés de compte statiques
account_keysnum_account_keys x 32 octetsClés publiques
recent_blockhash32 octetsBlockhash
num_instructions1-3 octets (compact-u16)Nombre d'instructions
instructionsvariableTableau d'instructions compilées

Chaque instruction compilée est sérialisée comme suit :

ChampTailleDescription
program_id_index1 octetIndex dans les clés de compte
num_accounts1-3 octets (compact-u16)Nombre d'indices de compte
account_indicesnum_accounts x 1 octetIndices de clés de compte
data_len1-3 octets (compact-u16)Longueur des données d'instruction
datadata_len octetsDonné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 SOLDiagramme 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 SOLDiagramme du processus de transfert de SOL

L'exemple ci-dessous montre le code pertinent pour les diagrammes ci-dessus. Voir la fonction transfer du System Program.

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Create a connection to cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Fund sender with airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { value: preBalance1 } = await rpc.getBalance(sender.address).send();
const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
// Send the transaction to the network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { value: postBalance1 } = await rpc.getBalance(sender.address).send();
const { value: postBalance2 } = await 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);
Console
Click to execute the code.

L'exemple suivant montre la structure d'une transaction qui contient une seule instruction de transfert de SOL.

import {
createSolanaRpc,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("http://localhost:8899");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
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 = getTransferSolInstruction({
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.

Le code ci-dessous montre la sortie des extraits de code précédents. Le format diffère entre 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
}
}
]
}

Récupération des détails de transaction

Après la soumission, récupérez les détails de la transaction en utilisant la signature de transaction et la méthode RPC getTransaction.

Vous pouvez également trouver la transaction en utilisant 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"
}

Is this page helpful?

Table des matières

Modifier la page

Géré par

© 2026 Fondation Solana.
Tous droits réservés.
Restez connecté