Pipeline de transaction

Résumé

Les transactions passent par 8 étapes : réception, sigverify, sanitize, vérifications budget/âge, validation du payeur de frais, chargement de compte, exécution d'instruction et validation.

Pipeline de traitement des transactions

Lorsqu'une transaction arrive à un validator, elle passe par une série d'étapes de validation et d'exécution. Ce qui suit décrit le pipeline complet de la réception à la validation, avec des références aux fichiers sources du client validator agave.

1. Réception et désérialisation

Le validator reçoit les octets de transaction via UDP/QUIC. Les octets bruts doivent tenir dans un seul paquet (PACKET_DATA_SIZE = 1 232 octets). Les octets sont désérialisés en une VersionedTransaction, qui contient le tableau de signatures et un VersionedMessage (legacy ou v0).

2. Vérification de signature (sigverify)

Les signatures sont vérifiées dans l'étape sigverify avant que la transaction n'entre dans l'étape banking. Pour chaque signature à l'index i, le vérificateur contrôle Ed25519(signatures[i], account_keys[i], message_bytes). Si une signature est invalide, le paquet est rejeté.

La vérification est parallélisée : le validator divise les lots de paquets en morceaux de VERIFY_PACKET_CHUNK_SIZE (128) et les traite en parallèle.

3. Sanitize

La transaction désérialisée est nettoyée pour produire une SanitizedTransaction (ou RuntimeTransaction). La sanitization valide les invariants structurels :

  • Le nombre de signatures correspond à num_required_signatures dans l'en-tête
  • Tous les program_id_index et account_indices d'instruction sont dans les limites
  • Le payeur de frais (index de compte 0) est un signataire modifiable

Le wrapper RuntimeTransaction met en cache les métadonnées précalculées depuis TransactionMeta : le hash du message, le flag de transaction de vote, les comptages de signatures de précompilation (Ed25519/secp256k1/secp256r1), les détails d'instruction du budget de calcul et la longueur totale des données d'instruction.

4. Vérifier le budget de calcul, l'âge et le cache de statut

La méthode check_transactions effectue plusieurs vérifications par transaction :

Budget de calcul : les instructions de budget de calcul de la transaction sont d'abord analysées et validées. Les détails des frais sont calculés à partir des limites du budget et des frais de priorisation. Si le budget de calcul est invalide ou contradictoire, la transaction échoue avec des erreurs d'analyse du budget de calcul telles que DuplicateInstruction, InstructionError(..., InvalidInstructionData), ou InvalidLoadedAccountsDataSizeLimit.

Âge du blockhash : le recent_blockhash de la transaction est recherché dans la BlockhashQueue. Si le hash est trouvé et que son âge est dans les MAX_PROCESSING_AGE (150 slots), la transaction se poursuit. S'il n'est pas trouvé, le validator vérifie la présence d'un nonce durable valide.

Cache de statut : le hash du message de la transaction est vérifié dans un cache de statut. S'il est trouvé, la transaction est rejetée avec AlreadyProcessed.

5. Valider le nonce et le payeur de frais

La méthode validate_transaction_nonce_and_fee_payer dans la SVM gère deux validations :

Validation du nonce (le cas échéant) : pour les transactions avec nonce, le validator charge le compte nonce et vérifie :

  • Le compte appartient au System Program
  • Il s'analyse comme State::Initialized
  • Le nonce durable stocké correspond au recent_blockhash de la transaction
  • Le nonce peut être avancé (son nonce durable actuel diffère du prochain nonce durable, c'est-à-dire que le nonce n'a pas déjà été utilisé dans le bloc actuel)
  • L'autorité du nonce a signé la transaction

S'il est valide, le nonce est avancé à la valeur du prochain nonce durable. Voir validate_transaction_nonce.

Validation du payeur de frais : le compte du payeur de frais (toujours à l'index 0) est chargé et vérifié par validate_fee_payer :

  • Le compte doit exister (lamports > 0), sinon AccountNotFound
  • Le compte doit être un compte système ou un compte nonce, sinon InvalidAccountForFee
  • Les lamports doivent couvrir min_balance + total_fee, où min_balance est 0 pour les comptes système ou rent.minimum_balance(NonceState::size()) pour les comptes nonce ; sinon InsufficientFundsForFee
  • Après déduction des frais, le compte doit rester exempté de loyer (ne peut pas passer d'exempté de loyer à payant de loyer)

Les frais sont déduits du payeur de frais à cette étape. Un instantané du payeur de frais après déduction des frais (et du nonce avancé, le cas échéant) est enregistré en tant que RollbackAccounts, qui sont les comptes qui sont validés même si l'exécution échoue.

6. Charger les comptes

load_transaction charge tous les comptes référencés par la transaction. Le AccountLoader enveloppe le stockage de comptes externe et maintient un cache local au lot afin que les comptes modifiés par les transactions précédentes dans le même lot soient visibles pour les suivantes.

Pour chaque compte non-payeur de frais, le chargeur :

  1. Récupère le compte depuis le cache ou la base de données des comptes
  2. Met à jour le statut d'exonération de loyer si nécessaire
  3. Accumule la taille des données du compte vers le loaded_accounts_data_size_limit (64 Mio par défaut). Chaque compte entraîne un surcoût de base de TRANSACTION_ACCOUNT_BASE_SIZE (64 octets) plus la longueur de ses données

Pour chaque programme invoqué par les instructions de la transaction, le chargeur vérifie que le compte du programme existe et appartient à un chargeur valide (NativeLoader ou l'un des PROGRAM_OWNERS). Les programmes invalides échouent avec ProgramAccountNotFound ou InvalidProgramForExecution.

Les programmes LoaderV3 (évolutifs) chargent implicitement leur compte programdata associé, qui compte également dans la limite de taille des données chargées.

Si le chargement du compte échoue mais que le payeur de frais a été validé avec succès, la transaction devient un résultat FeesOnly : les frais sont toujours collectés mais aucune instruction n'est exécutée.

7. Exécuter les instructions

execute_loaded_transaction crée un TransactionContext avec tous les comptes chargés et invoque process_message. Les instructions s'exécutent séquentiellement dans l'ordre où elles apparaissent dans le message. Chaque invocation d'instruction crée un InvokeContext et appelle le programme cible.

Détails du traitement des instructions

La fonction process_message du runtime parcourt chaque instruction et appelle le programme cible :

  1. Pour chaque instruction, le runtime appelle prepare_next_top_level_instruction, qui construit le InstructionContext. Ce contexte contient des références aux comptes de l'instruction (résolus à partir des indices compilés), les données de l'instruction et l'index du compte du programme.
  2. Le runtime vérifie si le programme est un precompile (Ed25519, Secp256k1, Secp256r1). Les precompiles sont vérifiés directement sans invoquer la VM BPF.
  3. Pour tous les autres programmes, le runtime invoque process_instruction, qui charge le programme depuis le cache et l'exécute dans la machine virtuelle BPF.
  4. Une fois l'instruction terminée, le runtime vérifie que le solde total en lamports de tous les comptes de l'instruction n'a pas changé (vérification UnbalancedInstruction).
  5. Si une instruction échoue, la transaction entière est annulée. Aucun changement d'état intermédiaire n'est validé.

Chaque instruction incrémente la trace d'instruction. La trace inclut à la fois les instructions de niveau supérieur et tous les CPI qu'elles invoquent. La longueur totale de la trace (instructions de niveau supérieur plus tous les CPI imbriqués) ne peut pas dépasser 64 (MAX_INSTRUCTION_TRACE_LENGTH). Dépasser cette limite renvoie InstructionError::MaxInstructionTraceLengthExceeded.

Après l'exécution, le runtime vérifie que :

  • La somme des lamports sur tous les comptes n'a pas changé
  • Aucun compte n'est passé d'un état exempt de loyer à un état payant un loyer

8. Validation ou annulation

Si l'exécution réussit, les états de compte modifiés du TransactionContext sont écrits dans le cache local du batch du AccountLoader. Si l'exécution échoue, seuls les RollbackAccounts (payeur de frais avec frais déduits et nonce avancé) sont écrits. Les frais sont toujours collectés, mais tous les autres changements de compte sont annulés.

Résumé du pipeline

Receive packet (UDP/QUIC)
--> Deserialize into VersionedTransaction
--> Sigverify (parallel Ed25519 verification)
--> Sanitize (structural validation, metadata extraction)
--> Parse compute budget, calculate fees
--> Check blockhash age (or verify nonce account)
--> Check status cache (dedup)
--> Validate nonce authority and advanceability (if nonce transaction)
--> Validate fee payer (load, check balance, deduct fee)
--> Load all accounts (with data size limits)
--> Load programs (verify loaders)
--> Execute instructions sequentially
--> Verify post-conditions (lamport balance, rent state)
--> Commit account changes (or rollback on failure)

Référence des erreurs de transaction

Le tableau suivant liste toutes les variantes de TransactionError et à quelle étape du pipeline elles se produisent :

ErreurÉtapeCause
AccountInUsePlanificationLe compte est déjà verrouillé par une autre transaction dans le même lot
AccountLoadedTwicePlanificationUne pubkey apparaît deux fois dans le account_keys de la transaction
AccountNotFoundValidation du payeur de fraisLe compte du payeur de frais n'existe pas
ProgramAccountNotFoundChargement de compteUn programme invoqué n'existe pas
InsufficientFundsForFeeValidation du payeur de fraisLe payeur de frais ne peut pas couvrir les frais + le minimum exempt de loyer
InvalidAccountForFeeValidation du payeur de fraisLe payeur de frais n'est pas un compte système ou nonce
AlreadyProcessedCache de statutLa transaction a déjà été traitée
BlockhashNotFoundVérification d'âgeLe blockhash n'est pas dans la file d'attente et n'est pas un nonce valide
InstructionErrorExécutionUne erreur s'est produite lors du traitement d'une instruction (inclut l'index d'instruction et le InstructionError spécifique)
CallChainTooDeepChargement de compteLa chaîne d'appel du chargeur est trop profonde
MissingSignatureForFeeAssainissementLa transaction nécessite des frais mais n'a pas de signature présente
InvalidAccountIndexAssainissementLa transaction contient une référence de compte invalide
SignatureFailureVérification de signatureLa signature Ed25519 ne se vérifie pas (le paquet est rejeté)
InvalidProgramForExecutionChargement de compteLe programme n'appartient pas à un chargeur valide
SanitizeFailureAssainissementLa transaction n'a pas réussi à assainir correctement les décalages de comptes
ClusterMaintenancePlanificationLes transactions sont actuellement désactivées en raison de la maintenance du cluster
AccountBorrowOutstandingExécutionLe traitement de la transaction a laissé un compte avec une référence empruntée en suspens
WouldExceedMaxBlockCostLimitPlanificationLa transaction dépasserait la limite de coût maximum du bloc
UnsupportedVersionAssainissementLa version de la transaction n'est pas prise en charge
InvalidWritableAccountChargement de compteLa transaction charge un compte inscriptible qui ne peut pas être écrit
WouldExceedMaxAccountCostLimitPlanificationLa transaction dépasserait la limite de coût maximum de compte dans le bloc
WouldExceedAccountDataBlockLimitPlanificationLa transaction dépasserait la limite de données de compte dans le bloc
TooManyAccountLocksPlanificationLa transaction a verrouillé trop de comptes
AddressLookupTableNotFoundChargement de compteLe compte de table de recherche d'adresses n'existe pas
InvalidAddressLookupTableOwnerChargement de compteLa table de recherche d'adresses appartient au mauvais programme
InvalidAddressLookupTableDataChargement de compteLa table de recherche d'adresses contient des données invalides
InvalidAddressLookupTableIndexChargement de compteLa recherche de table d'adresses utilise un index invalide
InvalidRentPayingAccountVérification post-exécutionLe compte est passé d'exempt de loyer à payant de loyer
WouldExceedMaxVoteCostLimitPlanificationLa transaction dépasserait la limite de coût maximum de vote
WouldExceedAccountDataTotalLimitPlanificationLa transaction dépasserait la limite totale de données de compte
DuplicateInstructionAnalyse du budget de calculVariante d'instruction de budget de calcul dupliquée dans la même transaction
InsufficientFundsForRentVérification post-exécutionLe compte n'a pas assez de lamports pour couvrir le loyer de sa taille de données
MaxLoadedAccountsDataSizeExceededChargement de compteLe total des données chargées dépasse la limite de 64 Mio
InvalidLoadedAccountsDataSizeLimitAnalyse du budget de calculSetLoadedAccountsDataSizeLimit défini à 0
ResanitizationNeededAssainissementLa transaction différait avant/après l'activation de la fonctionnalité et nécessite un réassainissement
ProgramExecutionTemporarilyRestrictedChargement de compteL'exécution du programme est temporairement restreinte sur le compte référencé
UnbalancedTransactionVérification post-exécutionLe solde total de lamports avant la transaction n'est pas égal au solde après
ProgramCacheHitMaxLimitChargement de compteLe cache du programme a atteint la limite maximale
CommitCancelledValidationValidation annulée en interne

Is this page helpful?

Géré par

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