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
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 clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst 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 airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { 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 networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { 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);
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 keypairsconst sender = await generateKeyPairSigner();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 = getTransferSolInstruction({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 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.
{"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?