Modèles UTXO et par compte · Leçon 4 sur 6
Reconstituer les soldes et les historiques
9 min de lecture
« Quel montant cette adresse détenait-elle à cette date, et comment l’a-t-elle obtenu ? » C’est l’une des questions les plus fréquentes auxquelles un analyste doit répondre, que ce soit pour l’examen des risques d’une plateforme d’échange, le rapport d’un expert désigné par un tribunal ou un article de journaliste. Il existe deux façons d’y répondre : calculer le solde à partir de l’historique ou le lire dans l’état de la chaîne. Un analyste rigoureux fait les deux et explique toute différence. Cette leçon montre comment procéder pour les cryptomonnaies UTXO, les soldes natifs de comptes et les jetons.
Deux façons d’obtenir un solde
| Approche | Méthode | Points forts | Points faibles |
|---|---|---|---|
| Calcul à partir de l’historique | Additionner tous les crédits et débits jusqu’à un bloc | Explique chaque mouvement ; fonctionne dans un entrepôt de données | Il est facile d’oublier une catégorie de mouvements |
| Lecture de l’état | Demander le solde à un nœud pour un bloc | Fait autorité pour cette chaîne et ce bloc | N’apporte aucune explication ; les anciens blocs nécessitent un état d’archive |
Le chiffre calculé est celui qu’on inclut dans un rapport, car il raconte l’historique. Le chiffre de l’état sert de vérification. Quand les deux concordent, l’historique est complet ; quand ils diffèrent, il manque quelque chose, et l’ampleur de l’écart indique généralement quoi.
Soldes UTXO
Sur une chaîne UTXO, le solde d’un ensemble de scripts à la hauteur H correspond à la somme des sorties vers ces scripts créées à la hauteur H ou avant, et non dépensées à la hauteur H ou avant (voir la leçon Le modèle UTXO pour les analystes). Voici un exemple détaillé pour une adresse :
| Point de sortie | Créé à la hauteur | Montant (BTC) | Dépensé à la hauteur |
|---|---|---|---|
4f3a…c21e:1 | 880,100 | 0,4000 | 880,900 |
a91c…3b07:0 | 880,450 | 1,2500 | non dépensé |
77d2…e4a0:3 | 881,020 | 0,0800 | 885,300 |
c0b5…91f2:0 | 884,700 | 0,3000 | non dépensé |
À la hauteur 882,000, les sorties non dépensées sont a91c…3b07:0 et 77d2…e4a0:3 : 1,33 BTC. À la hauteur 886,000, il s’agit de a91c…3b07:0 et c0b5…91f2:0 : 1,55 BTC. Le total reçu est de 2,03 BTC et le total envoyé de 0,48 BTC. À la hauteur 886,000, la différence entre les montants reçus et envoyés donne le même solde de 1,55 BTC : une vérification interne utile.
Points à surveiller :
- Un portefeuille n’est pas une adresse. Les avoirs d’une personne sont répartis entre de nombreuses adresses et souvent plusieurs types de scripts. Si les descripteurs de sortie ou la clé publique étendue d’un portefeuille sont connus (par exemple, s’ils sont fournis par le propriétaire dans le cadre d’un litige), dérivez toutes ses adresses, y compris les adresses de change, avant de faire la somme.
- Données non confirmées et réorganisées. Les transactions du mempool ne constituent pas des soldes. Prenez un instantané à une hauteur suffisamment profonde pour être stable (voir la leçon Les blocs et les en-têtes comme données).
- Les sorties coinbase datant de moins de 100 blocs existent, mais ne peuvent pas encore être dépensées ; précisez-le si le solde d’un mineur est en question.
- Les vérifications de l’état sont limitées :
scantxoutsetde Bitcoin Core ne consulte que l’ensemble UTXO actuel. Les vérifications historiques nécessitent donc un indexeur ou un deuxième jeu de données constitué indépendamment.
Soldes natifs sur les chaînes à comptes
Pour l’ETH et les autres monnaies natives, un solde au bloc N se décompose en catégories réparties dans différents jeux de données :
balance(N) = valeur reçue dans les transactions de premier niveau réussies
+ valeur reçue dans les appels internes réussis (traces)
+ retraits de la couche de consensus et récompenses du destinataire des frais
- valeur envoyée dans les transactions de premier niveau et les appels internes réussis
- frais de gas payés pour chaque transaction envoyée, réussie ou échouée
(Les allocations de genèse et les récompenses de minage avant la fusion ajoutent des termes pour les anciennes adresses.) Voici un exemple détaillé pour 0xA1b2…9F au bloc N :
| Élément | Source | Montant (ETH) |
|---|---|---|
| Dépôt depuis une plateforme d’échange | Transaction | +5,000000 |
| Versement depuis un contrat | Trace (appel interne) | +1,000000 |
| Retraits du validateur | Liste des retraits du bloc | +0,032000 |
| Paiement envoyé | Transaction | −2,500000 |
| Gas pour ce paiement : 21 000 × 20 gwei | Reçu | −0,000420 |
| Transaction échouée : valeur non envoyée | status du reçu = 0 | 0 |
| Gas pour la transaction échouée : 70 000 × 30 gwei | Reçu | −0,002100 |
| Solde calculé | 3,529480 |
Si eth_getBalance(0xA1b2…9F, N) renvoie 3,529480 ETH, l’historique est complet. Les erreurs courantes laissent une empreinte dans l’écart. Ignorer les traces sous-estime le solde exactement du montant du crédit interne (1,0). Comptabiliser la valeur de la transaction échouée la déduit à tort ; oublier son gas surestime le solde de 0,0021. L’absence des retraits crée un écart constitué de petits montants réguliers. Le gas correspond au gas utilisé multiplié par le prix effectif du gas indiqué dans le reçu, et non à la limite de gas, auxquels s’ajoutent les frais des blobs pour les transactions de type 3.
Soldes de jetons : événements et balanceOf
Les soldes de jetons sont stockés dans le contrat du jeton. Ils peuvent être calculés en additionnant les événements Transfer, ou lus avec balanceOf(holder) via eth_call au bloc N. Voici la version calculée en SQL, à partir d’une table des transferts décodés :
-- Soldes des jetons au bloc 21 000 000, calculés à partir des événements Transfer
WITH moves AS (
SELECT to_address AS holder, amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
UNION ALL
SELECT from_address AS holder, -amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
)
SELECT holder, SUM(delta) / POWER(10, 6) AS balance -- le jeton a 6 décimales
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Chaque transfert devient deux lignes : un crédit au destinataire et un débit à l’expéditeur. La somme par détenteur correspond au solde. Les créations de jetons apparaissent comme des transferts depuis l’adresse zéro ; celle-ci finit donc avec un solde négatif égal à tous les jetons créés, moins ceux qui y ont été brûlés : une vérification intégrée de l’offre. Conservez les montants entiers bruts jusqu’à la division finale ; convertir chaque ligne en décimales au préalable entraîne une accumulation des erreurs d’arrondi. Utilisez un type décimal de grande capacité, car les montants bruts peuvent dépasser les entiers de 64 bits.
Les événements et balanceOf peuvent diverger pour des raisons connues :
- Les jetons à réajustement modifient le solde de chaque détenteur au moyen d’un multiplicateur global, sans événement
Transferpar détenteur. Les sommes des événements deviennent immédiatement obsolètes. - Les jetons portant intérêt calculent
balanceOfà partir d’un indice croissant ; les événements enregistrent les mouvements de principal, pas les intérêts accumulés. - Les jetons à frais de transfert créditent le destinataire d’un montant inférieur à celui envoyé par l’expéditeur ; la plupart émettent des événements pour les montants nets, mais certains émettent un événement pour le montant brut.
- Les contrats non standard, surtout les plus anciens, créent des jetons sans émettre d’événement
Transfer, ou les fonctions de gel et de destruction de l’émetteur émettent plutôt un événement personnalisé.
Pour ces jetons, l’état fait foi et les événements ne racontent que les transferts explicites ; la leçon Lire les transferts de jetons les examine en détail.
Instantanés à une hauteur de bloc
Un solde « au 31 mars » est ambigu tant qu’il n’est pas rattaché à un bloc. Convertissez l’heure en dernier bloc dont l’horodatage est antérieur ou égal à l’heure limite, en UTC, et consignez à la fois la hauteur et le hachage du bloc. Sur Ethereum, les méthodes d’accès à l’état telles que eth_getBalance et eth_call acceptent un numéro de bloc ou, selon EIP-1898, un hachage de bloc, ce qui garantit que la réponse provient de ce bloc précis, même si une réorganisation remplace ensuite le bloc à cette hauteur. Pour interroger des blocs anciens, il faut un nœud qui conserve l’état historique (un nœud d’archive ou un fournisseur).
Pour les instantanés multichaînes, chaque chaîne a son propre bloc pour un même instant : les blocs de Bitcoin, espacés d’environ 10 minutes, et les créneaux d’Ethereum, espacés de 12 secondes, offrent une granularité différente. Un bloc L2 d’un rollup peut refléter des dépôts que sa L1 vient tout juste d’inclure. Indiquez dans le rapport le bloc choisi pour chaque chaîne.
Vérifications de rapprochement
Une reconstitution n’est terminée qu’après vérification. Voici quelques tests utiles :
- Calcul par rapport à l’état : comparez les soldes calculés aux soldes de l’état du nœud pour chaque adresse concernée, ou à un échantillon aléatoire réparti sur plusieurs blocs pour les grands ensembles.
- Conservation : la somme de tous les soldes de jetons calculés, à l’exclusion de l’adresse zéro, doit être égale à
totalSupply()au même bloc ; sur une chaîne UTXO, le total de l’ensemble UTXO ne doit pas dépasser l’émission totale. - Absence de soldes négatifs : un solde cumulé négatif indique un crédit manquant, le plus souvent un appel interne, une création de jetons sans événement ou un retrait.
- Limites : le solde avant la première activité d’une adresse est nul, et le solde après une consolidation connue correspond à ce qu’elle a laissé.
- Écarts résiduels expliqués : définissez à l’avance une tolérance (par exemple, zéro pour les soldes natifs, un montant indiqué pour les jetons à réajustement) et documentez la cause de chaque différence.
| Symptôme | Cause probable |
|---|---|
| Solde calculé trop faible, l’écart correspondant aux montants des versements de contrats | Appels internes manquants dans les traces |
| Solde calculé trop élevé de petits montants | Frais de gas de transactions échouées non déduits |
| Petits crédits réguliers inexpliqués | Retraits de la couche de consensus ou récompenses du destinataire des frais |
Les soldes des détenteurs de jetons s’écartent chaque jour de balanceOf | Jeton à réajustement ou portant intérêt |
La somme des soldes diffère de totalSupply() | Créations ou destructions de jetons sans événement Transfer |
Points clés pour l’examen : la différence entre calculer un solde à partir de l’historique et le lire dans l’état, et les raisons pour lesquelles les rapports doivent inclure les deux ; calculer un solde UTXO à une hauteur donnée et vérifier la différence entre les montants reçus et envoyés ; les composantes d’un solde natif de compte, notamment les appels internes, les retraits et le gas des transactions échouées ; calculer le gas en multipliant le gas utilisé par le prix effectif du gas ; calculer les soldes des jetons à partir des événements Transfer et vérifier l’offre à l’aide de l’adresse zéro ; les raisons pour lesquelles les jetons à réajustement, portant intérêt et à frais de transfert faussent les sommes des événements ; associer les instantanés à une hauteur et à un hachage de bloc (EIP-1898) ; les vérifications de rapprochement standard et ce que suggère chaque type d’écart.