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_signaturesdans l'en-tête - Tous les
program_id_indexetaccount_indicesd'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_blockhashde 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_balanceest 0 pour les comptes système ourent.minimum_balance(NonceState::size())pour les comptes nonce ; sinonInsufficientFundsForFee - 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 :
- Récupère le compte depuis le cache ou la base de données des comptes
- Met à jour le statut d'exonération de loyer si nécessaire
- 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 deTRANSACTION_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 :
- Pour chaque instruction, le runtime appelle
prepare_next_top_level_instruction, qui construit leInstructionContext. 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. - 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.
- 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. - 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). - 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 | Étape | Cause |
|---|---|---|
AccountInUse | Planification | Le compte est déjà verrouillé par une autre transaction dans le même lot |
AccountLoadedTwice | Planification | Une pubkey apparaît deux fois dans le account_keys de la transaction |
AccountNotFound | Validation du payeur de frais | Le compte du payeur de frais n'existe pas |
ProgramAccountNotFound | Chargement de compte | Un programme invoqué n'existe pas |
InsufficientFundsForFee | Validation du payeur de frais | Le payeur de frais ne peut pas couvrir les frais + le minimum exempt de loyer |
InvalidAccountForFee | Validation du payeur de frais | Le payeur de frais n'est pas un compte système ou nonce |
AlreadyProcessed | Cache de statut | La transaction a déjà été traitée |
BlockhashNotFound | Vérification d'âge | Le blockhash n'est pas dans la file d'attente et n'est pas un nonce valide |
InstructionError | Exécution | Une erreur s'est produite lors du traitement d'une instruction (inclut l'index d'instruction et le InstructionError spécifique) |
CallChainTooDeep | Chargement de compte | La chaîne d'appel du chargeur est trop profonde |
MissingSignatureForFee | Assainissement | La transaction nécessite des frais mais n'a pas de signature présente |
InvalidAccountIndex | Assainissement | La transaction contient une référence de compte invalide |
SignatureFailure | Vérification de signature | La signature Ed25519 ne se vérifie pas (le paquet est rejeté) |
InvalidProgramForExecution | Chargement de compte | Le programme n'appartient pas à un chargeur valide |
SanitizeFailure | Assainissement | La transaction n'a pas réussi à assainir correctement les décalages de comptes |
ClusterMaintenance | Planification | Les transactions sont actuellement désactivées en raison de la maintenance du cluster |
AccountBorrowOutstanding | Exécution | Le traitement de la transaction a laissé un compte avec une référence empruntée en suspens |
WouldExceedMaxBlockCostLimit | Planification | La transaction dépasserait la limite de coût maximum du bloc |
UnsupportedVersion | Assainissement | La version de la transaction n'est pas prise en charge |
InvalidWritableAccount | Chargement de compte | La transaction charge un compte inscriptible qui ne peut pas être écrit |
WouldExceedMaxAccountCostLimit | Planification | La transaction dépasserait la limite de coût maximum de compte dans le bloc |
WouldExceedAccountDataBlockLimit | Planification | La transaction dépasserait la limite de données de compte dans le bloc |
TooManyAccountLocks | Planification | La transaction a verrouillé trop de comptes |
AddressLookupTableNotFound | Chargement de compte | Le compte de table de recherche d'adresses n'existe pas |
InvalidAddressLookupTableOwner | Chargement de compte | La table de recherche d'adresses appartient au mauvais programme |
InvalidAddressLookupTableData | Chargement de compte | La table de recherche d'adresses contient des données invalides |
InvalidAddressLookupTableIndex | Chargement de compte | La recherche de table d'adresses utilise un index invalide |
InvalidRentPayingAccount | Vérification post-exécution | Le compte est passé d'exempt de loyer à payant de loyer |
WouldExceedMaxVoteCostLimit | Planification | La transaction dépasserait la limite de coût maximum de vote |
WouldExceedAccountDataTotalLimit | Planification | La transaction dépasserait la limite totale de données de compte |
DuplicateInstruction | Analyse du budget de calcul | Variante d'instruction de budget de calcul dupliquée dans la même transaction |
InsufficientFundsForRent | Vérification post-exécution | Le compte n'a pas assez de lamports pour couvrir le loyer de sa taille de données |
MaxLoadedAccountsDataSizeExceeded | Chargement de compte | Le total des données chargées dépasse la limite de 64 Mio |
InvalidLoadedAccountsDataSizeLimit | Analyse du budget de calcul | SetLoadedAccountsDataSizeLimit défini à 0 |
ResanitizationNeeded | Assainissement | La transaction différait avant/après l'activation de la fonctionnalité et nécessite un réassainissement |
ProgramExecutionTemporarilyRestricted | Chargement de compte | L'exécution du programme est temporairement restreinte sur le compte référencé |
UnbalancedTransaction | Vérification post-exécution | Le solde total de lamports avant la transaction n'est pas égal au solde après |
ProgramCacheHitMaxLimit | Chargement de compte | Le cache du programme a atteint la limite maximale |
CommitCancelled | Validation | Validation annulée en interne |
Is this page helpful?