Réessayer Des Transactions
Il arrive parfois qu'une transaction apparemment valide soit rejetée avant d'être incluse dans un bloc. Cela se produit le plus souvent pendant les périodes de congestion du réseau, lorsqu'un nœud RPC ne parvient pas à retransmettre la transaction au leader. Pour l'utilisateur final, il peut avoir l'impression que sa transaction disparaît complètement. Si les nœuds RPC sont équipés d'un algorithme de retransmission générique, les développeurs d'applications sont également capables de développer leur propre logique de retransmission personnalisée.
Faits
Fiche d'Information
- Les nœuds RPC tenteront de retransmettre les transactions en utilisant un algorithme générique
- ALes développeurs d'applications peuvent mettre en œuvre leur propre logique de retransmission personnalisée
- Les développeurs devraient tirer parti du paramètre
maxRetries
de la méthode JSON-RPCsendTransaction
- Les développeurs devraient activer des contrôles en amont afin de détecter les erreurs avant que les transactions ne soient soumises
- Avant de re-signer une transaction, il est très important de s'assurer que le blockhash de la transaction initiale a expiré
Le Voyage d'une Transaction
Comment Les Clients Soumettent Les Transactions
Chez Solana, il n'y a pas de concept de mempool.Toutes les transactions, qu'elles soient initiées par un programme ou par un utilisateur final, sont acheminées efficacement vers les leaders afin d'être traitées dans un bloc. Il existe deux manières principales d'envoyer une transaction aux leaders :
- Par proxy via un serveur RPC et la méthode JSON-RPC sendTransaction
- Directement aux leaders via un Client TPU
La grande majorité des utilisateurs finaux soumettront des transactions via un serveur RPC. Lorsqu'un client soumet une transaction, le nœud RPC récepteur tente à son tour de transmettre la transaction aux leaders actuels et suivants. Tant que la transaction n'est pas traitée par un leader, il n'y a pas d'enregistrement de la transaction en dehors de ce dont le client et les nœuds RPC relais ont connaissance. Dans le cas d'un client TPU, la retransmission et le transfert des leaders sont entièrement gérés par le logiciel client.
Comment Les Nœuds RPC Transmettent Les Transactions
Lorsqu'un nœud RPC reçoit une transaction via sendTransaction
, il la convertit en un paquet UDP avant de la transmettre aux leaders concernés. UDP permet aux validateurs de communiquer rapidement entre eux, mais ne fournit aucune garantie quant à la livraison des transactions.
Comme le calendrier des leaders de Solana est connu à l'avance pour chaque epoque (~2 jours), un noeud RPC diffusera sa transaction directement aux leaders actuels et suivants. Cela contraste avec d'autres protocoles de bavardage, comme Ethereum, qui propagent les transactions de manière aléatoire et à grande échelle sur l'ensemble du réseau. Par défaut, les nœuds RPC essaient de transmettre les transactions aux leaders toutes les deux secondes jusqu'à ce que la transaction soit finalisée ou que le hash de blocs de la transaction expire (150 blocs ou ~1 minute 19 secondes au moment de la rédaction de ce document). Si la taille de la file d'attente de retransmissions en attente est supérieure à 10,000 transactions les nouvelles transactions soumises sont rejetées. Il existe des arguments de ligne de commande que les opérateurs RPC peuvent ajuster pour modifier le comportement par défaut de cette logique de réessai.
Lorsqu'un nœud RPC transmet une transaction, il tente de la transmettre à l'Unité de Traitement des Transactions (Transaction Processing Unit ou TPU) d'un leader. Le TPU traite les transactions en cinq phases distinctes :
- Étape de Récupération (Fetch Stage)
- Étape de SigVerify (SigVerify Stage)
- Étape de Banking (Banking Stage)
- Service Proof of History
- Étape de Transmission (Broadcast Stage)
Avec l'Autorisation de Jito Labs
Parmi ces cinq phases, l'étape Fetch est responsable de la réception des transactions. Dans celle-ci, les validateurs classeront les transactions entrantes en fonction de trois ports :
- tpu gère les transactions régulières telles que les transferts de jetons, les mints de NFT et les instructions de programme
- tpu_vote se concentre exclusivement sur les transactions de vote
- tpu_forwards transmet les paquets non traités au leader suivant si le leader actuel est incapable de traiter toutes les transactions.
Pour plus d'informations sur le TPU, veuillez consulter cet excellent article de Jito Labs.
Comment Les Transactions Sont Rejetées
Tout au long du voyage d'une transaction, il existe quelques scénarios dans lesquels la transaction peut être involontairement rejetée du réseau.
Avant le traitement d'une transaction
Si le réseau rejette une transaction, il le fera très probablement avant que la transaction ne soit traitée par un leader. La perte de paquets UDP est la raison la plus simple pour laquelle cela peut se produire. En période de charge intense du réseau, il est également possible que les validateurs soient submergés par le nombre de transactions à traiter. Bien que les validateurs soient équipés pour transmettre les transactions excédentaires via tpu_forwards
, il y a une limite à la quantité de données qui peuvent être transmises. De plus, chaque transfert est limité à un seul saut entre les validateurs. En d'autres termes, les transactions reçues sur le port tpu_forwards
ne sont pas transmises à d'autres validateurs.
Il existe également deux raisons moins connues pour lesquelles une transaction peut être rejetée avant d'être traitée. Le premier scénario implique des transactions qui sont soumises via un pool RPC. Il arrive qu'une partie de la pool RPC soit suffisamment en avance sur le reste de la pool. Cela peut poser des problèmes lorsque les nœuds de la pool doivent travailler ensemble. Dans cet exemple, le recentBlockhash de la transaction est interrogé à partir de la partie avancée de la pool (Backend A). Lorsque la transaction est soumise à la partie en retard de la pool (Backend B), les nœuds ne reconnaîtront pas le blockhash avancé et rejetteront la transaction. Cela peut être détecté lors de la soumission de la transaction si les développeurs activent les contrôles préalables (preflight checks) de sendTransaction
.
Les forks temporaires du réseau peuvent également entraîner le rejet de transactions. Si un validateur est lent à rejouer ses blocs pendant l'Étape de Banking, il peut finir par créer un fork minoritaire. Quand un client crée une transaction, il est possible que la transaction fasse référence à un recentBlockhash
qui n'existe que sur le fork minoritaire. Après la soumission de la transaction, le cluster peut alors se détacher de son fork minoritaire avant que la transaction ne soit traitée. Dans ce scénario, la transaction est rejetée parce que le blockhash n'a pas été trouvé.
Après le traitement d'une transaction et avant sa finalisation
Dans le cas où une transaction fait référence à un recentBlockhash
d'un fork minoritaire, il est toujours possible que la transaction soit traitée. Dans ce cas, cependant, il serait traité par le leader du fork minoritaire. Lorsque ce leader tente de partager ses transactions traitées avec le reste du réseau, il ne parvient pas à obtenir un consensus avec la majorité des validateurs qui ne reconnaissent pas le fork minoritaire. A ce stade, la transaction serait rejetée avant d'être finalisée.
Traitement Des Transactions Rejetées
Bien que les nœuds RPC tentent de retransmettre les transactions, l'algorithme qu'ils utilisent est générique et souvent mal adapté aux besoins des applications spécifiques. Pour se préparer aux périodes de congestion du réseau, les développeurs d'applications doivent personnaliser leur propre logique de retransmission.
Un Examen Approfondi De sendTransaction
Lorsqu'il s'agit de soumettre des transactions, la méthode RPC sendTransaction
est le principal outil à la disposition des développeurs. sendTransaction
est seulement chargé de relayer une transaction d'un client vers un noeud RPC. Si le noeud reçoit la transaction, sendTransaction
retournera l'identifiant de la transaction qui peut être utilisé pour suivre la transaction. Une réponse positive n'indique pas si la transaction sera traitée ou finalisée par le cluster.
TIP
Paramètres De La Requête
transaction
:string
- Transaction entièrement signée, sous forme de chaîne de caractères codée- (facultatif)
configuration object
:object
skipPreflight
:boolean
- si true, ignore les contrôles préalables de la transaction (par défaut : false)- (facultatif)
preflightCommitment
:string
- Commitment Niveau à utiliser pour les simulations préalables par rapport au slot bank (par défaut : "finalized"). - (facultatif)
encoding
:string
- Encodage utilisé pour les données de la transaction. Soit "base58" (lent), soit "base64". (par défaut : "base58"). - (facultatif)
maxRetries
:usize
- Nombre maximal de fois où le nœud RPC doit réessayer d'envoyer la transaction au leader. Si ce paramètre n'est pas fourni, le nœud RPC réessayera la transaction jusqu'à ce qu'elle soit finalisée ou jusqu'à ce que le blockhash expire.
Réponse
transaction id
:string
- Première signature de transaction incorporée dans la transaction, sous forme de chaîne de caractères codée en base-58. Cet identifiant de transaction peut être utilisé avec getSignatureStatuses pour demander des mises à jour de statut.
Personnalisation De La logique De Retransmission
Afin de développer leur propre logique de retransmission, les développeurs doivent tirer parti du paramètre maxRetries
de sendTransaction
. S'il est fourni, maxRetries
remplacera la logique de ré-essai par défaut d'un nœud RPC, permettant aux développeurs de contrôler manuellement le processus de ré-essai dans des limites raisonnables.
Un schéma courant pour réessayer manuellement les transactions implique le stockage temporaire de lastValidBlockHeight
qui provient de getLatestBlockhash. Une fois stockée, une application peut alors interroger la hauteur de bloc du cluster (cluster’s blockheight) et réessayer manuellement la transaction à un moment approprié. En cas de congestion du réseau, il est avantageux de mettre maxRetries
à 0 et de retransmettre manuellement via un algorithme personnalisé. Alors que certaines applications peuvent utiliser un algorithme de backoff exponentiel, d'autres, comme [Mango],(https://www.mango.markets/) choisissent de resoumettre continuellement les transactions à un intervalle constant jusqu'à ce qu'un certain délai se soit écoulé.
import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";
const sleep = async (ms: number) => {
return new Promise((r) => setTimeout(r, ms));
};
(async () => {
const payer = Keypair.generate();
const toAccount = Keypair.generate().publicKey;
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
const blockhashResponse = await connection.getLatestBlockhashAndContext();
const lastValidBlockHeight = blockhashResponse.context.slot + 150;
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.value.blockhash,
lastValidBlockHeight: lastValidBlockHeight,
}).add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount,
lamports: 1000000,
})
);
const message = transaction.serializeMessage();
const signature = nacl.sign.detached(message, payer.secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
const rawTransaction = transaction.serialize();
let blockheight = await connection.getBlockHeight();
while (blockheight < lastValidBlockHeight) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await sleep(500);
blockheight = await connection.getBlockHeight();
}
})();
while (blockheight < lastValidBlockHeight) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await sleep(500);
blockheight = await connection.getBlockHeight();
}
Lors de l'interrogation via getLatestBlockhash
, les applications doivent spécifier leur niveau d'engagement (commitment) prévu. En définissant son engagement sur confirmed
(voté) ou finalized
(~30 blocs après confirmed
), une application peut éviter d'interroger un blockhash d'un fork minoritaire.
Si une application a accès aux nœuds RPC derrière un équilibreur de charge, elle peut également choisir de répartir sa charge de travail entre des nœuds spécifiques. Les nœuds RPC qui servent des requêtes à forte intensité de données telles que getProgramAccounts peuvent être enclins à prendre du retard et ne sont pas adaptés à la transmission des transactions. Pour les applications qui gèrent des transactions sensibles au temps, il peut être prudent d'avoir des noeuds dédiés qui ne gèrent que sendTransaction
.
Le Coût de l'Omission Du Contrôle Préalable
Par défaut, sendTransaction
effectue trois contrôles préalables avant de soumettre une transaction. Plus précisément, sendTransaction
va :
- Vérifier que toutes les signatures sont valides
- Vérifier que le blockhash référencé se situe dans les 150 derniers blocs.
- Simuler la transaction sur le slot bank spécifié par le
preflightCommitment
Si l'un de ces trois contrôles préalables échoue, sendTransaction
déclenchera une erreur avant de soumettre la transaction. Les contrôles préalables peuvent souvent faire la différence entre le rejet d'une transaction et la possibilité pour un client de gérer gracieusement une erreur. Pour s'assurer que ces erreurs courantes sont prises en compte, il est recommandé aux développeurs de laisser la valeur skipPreflight
sur false
.
Quand Re-Signer Des Transactions
Malgré toutes les tentatives de retransmission, il peut arriver qu'un client soit obligé de re-signer une transaction. Avant de resigner une transaction, il est très important de s'assurer que le blockhash de la transaction initiale a expiré. Si le blockhash initial est toujours valide, il est possible que les deux transactions soient acceptées par le réseau. Pour un utilisateur final, cela donnerait l'impression qu'il a involontairement envoyé deux fois la même transaction.
Sur Solana, une transaction rejetée peut être éliminée en toute sécurité lorsque le blockhash qu'elle référence est plus ancien que le lastValidBlockHeight
reçu de getLatestBlockhash
. Les développeurs doivent garder la trace de ce lastValidBlockHeight
en interrogeant getEpochInfo
et en le comparant avec blockHeight
de la réponse. Une fois qu'un blockhash est invalidé, les clients peuvent resigner avec un nouveau blockhash.
Remerciements
Un grand merci à Trent Nelson, Jacob Creech, White Tiger, Le Yafo, Buffalu, and Jito Labs pour leur examen et leurs commentaires.