UTXO- und Kontenmodelle · Lektion 4 von 6
Salden und Verläufe rekonstruieren
8 Min. Lesezeit
„Wie viel hielt diese Adresse an diesem Datum, und wie kam es dazu?“ ist eine der häufigsten Fragen, die Analysten gestellt werden – sei es für die Risikoprüfung einer Börse, einen Bericht eines gerichtlich bestellten Sachverständigen oder eine journalistische Recherche. Es gibt zwei Wege, die Frage zu beantworten: den Saldo aus dem Verlauf abzuleiten oder ihn aus dem Chain-Zustand auszulesen. Ein sorgfältiger Analyst tut beides und erklärt etwaige Abweichungen. Diese Lektion zeigt, wie das bei UTXO-Coins, nativen Kontosalden und Token funktioniert.
Zwei Wege zum Saldo
| Weg | Vorgehensweise | Stärken | Schwächen |
|---|---|---|---|
| Aus dem Verlauf ableiten | Alle Gutschriften und Belastungen bis zu einem Block zusammenzählen | Erklärt jede Bewegung; funktioniert in einem Data Warehouse | Eine Bewegungskategorie lässt sich leicht übersehen |
| Aus dem Zustand auslesen | Einen Node nach dem Saldo an einem Block fragen | Für diese Chain und diesen Block maßgeblich | Keine Erklärung; für alte Blöcke wird ein Archivzustand benötigt |
Der abgeleitete Wert kommt in einen Bericht, weil er die Geschichte erzählt. Der Zustandswert dient als Gegenprobe. Stimmen die Werte überein, ist der Verlauf vollständig; unterscheiden sie sich, fehlt etwas, und die Größe der Lücke gibt meist einen Hinweis darauf, was fehlt.
UTXO-Salden
Bei einer UTXO-Chain entspricht der Saldo einer Menge von Skripten auf Höhe H der Summe der Ausgaben an diese Skripte, die auf oder vor H erstellt und nicht auf oder vor H ausgegeben wurden (siehe die Lektion Das UTXO-Modell für Analysten). Ein durchgerechnetes Beispiel für eine Adresse:
| Outpoint | Erstellt bei | Betrag (BTC) | Ausgegeben bei |
|---|---|---|---|
4f3a…c21e:1 | 880.100 | 0,4000 | 880.900 |
a91c…3b07:0 | 880.450 | 1,2500 | nicht ausgegeben |
77d2…e4a0:3 | 881.020 | 0,0800 | 885.300 |
c0b5…91f2:0 | 884.700 | 0,3000 | nicht ausgegeben |
Auf Höhe 882.000 sind die nicht ausgegebenen Ausgaben a91c…3b07:0 und 77d2…e4a0:3: 1,33 BTC. Auf Höhe 886.000 sind es a91c…3b07:0 und c0b5…91f2:0: 1,55 BTC. Insgesamt wurden 2,03 BTC empfangen und 0,48 BTC gesendet. Empfangene Beträge abzüglich gesendeter Beträge ergeben auf Höhe 886.000 ebenfalls 1,55 BTC: eine nützliche interne Gegenprobe.
Darauf ist zu achten:
- Eine Wallet ist keine Adresse. Die Bestände einer Person verteilen sich auf viele Adressen und oft auf mehrere Skripttypen. Sind die Output-Deskriptoren oder der erweiterte öffentliche Schlüssel einer Wallet bekannt (zum Beispiel, weil der Eigentümer sie bei einem Streitfall bereitgestellt hat), sollten vor dem Zusammenzählen alle zugehörigen Adressen abgeleitet werden, einschließlich Wechselgeldadressen.
- Unbestätigte Daten und Reorganisationen. Mempool-Transaktionen sind keine Salden. Erstelle den Snapshot auf einer ausreichend hohen, stabilen Höhe (siehe die Lektion Blöcke und Header als Daten).
- Coinbase-Ausgaben, die jünger als 100 Blöcke sind, existieren, können aber noch nicht ausgegeben werden; weise darauf hin, wenn der Saldo eines Miners untersucht wird.
- Zustandsprüfungen sind eingeschränkt:
scantxoutsetin Bitcoin Core berücksichtigt nur den aktuellen UTXO-Satz. Für historische Prüfungen werden daher ein Indexer oder ein zweiter, unabhängig erstellter Datensatz benötigt.
Native Salden auf Account-Chains
Bei ETH und anderen nativen Währungen lässt sich ein Saldo auf Block N in Kategorien aufteilen, die in unterschiedlichen Datensätzen gespeichert sind:
balance(N) = Wert, der in erfolgreichen Top-Level-Transaktionen empfangen wurde
+ Wert, der in erfolgreichen internen Aufrufen (Traces) empfangen wurde
+ Auszahlungen und Gebührenempfänger-Prämien auf der Konsensschicht
- Wert, der in erfolgreichen Top-Level-Transaktionen und internen Aufrufen gesendet wurde
- Gasgebühren für jede gesendete Transaktion, ob erfolgreich oder fehlgeschlagen
(Genesis-Zuteilungen und Mining-Prämien aus der Zeit vor dem Merge ergänzen weitere Bestandteile bei älteren Adressen.) Ein durchgerechnetes Beispiel für 0xA1b2…9F auf Block N:
| Posten | Quelle | Betrag (ETH) |
|---|---|---|
| Einzahlung von einer Börse | Transaktion | +5,000000 |
| Auszahlung aus einem Contract | Trace (interner Aufruf) | +1,000000 |
| Validator-Auszahlungen | Auszahlungs-Liste des Blocks | +0,032000 |
| Gesendete Zahlung | Transaktion | −2,500000 |
| Gas für diese Zahlung: 21.000 × 20 gwei | Receipt | −0,000420 |
| Fehlgeschlagene Transaktion: Wert nicht gesendet | Receipt status = 0 | 0 |
| Gas für die fehlgeschlagene Transaktion: 70.000 × 30 gwei | Receipt | −0,002100 |
| Abgeleiteter Saldo | 3,529480 |
Wenn eth_getBalance(0xA1b2…9F, N) 3,529480 ETH zurückgibt, ist der Verlauf vollständig. Typische Fehler hinterlassen erkennbare Spuren in der Differenz. Werden Traces ignoriert, ist der berechnete Saldo genau um die interne Gutschrift zu niedrig (1,0). Wird der Wert der fehlgeschlagenen Transaktion als gesendet gezählt, ist der berechnete Saldo um diesen Wert zu niedrig; werden die dafür angefallenen Gasgebühren vergessen, ist er um 0,0021 zu hoch. Fehlende Auszahlungen verursachen eine Lücke aus kleinen, regelmäßigen Beträgen. Die Gasgebühren berechnen sich aus dem verbrauchten Gas multipliziert mit dem effektiven Gaspreis aus dem Receipt, nicht aus dem Gaslimit; bei Transaktionen des Typs 3 kommen Blob-Gebühren hinzu.
Token-Salden: Events im Vergleich zu balanceOf
Token-Salden sind im Speicher des Token-Contracts abgelegt. Sie lassen sich berechnen, indem Transfer-Events summiert werden, oder über eth_call mit balanceOf(holder) auf Block N auslesen. Die abgeleitete Berechnung in SQL mit einer dekodierten Transfers-Tabelle:
-- Token-Salden auf Block 21.000.000, abgeleitet aus Transfer-Events
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 -- der Token hat 6 Dezimalstellen
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Jeder Transfer wird zu zwei Zeilen: einer Gutschrift für den Empfänger und einer Belastung für den Absender. Die Summe je Halter ergibt den Saldo. Mints erscheinen als Transfers von der Nulladresse. Daher weist die Nulladresse am Ende einen negativen Saldo auf, der der Gesamtmenge aller jemals geminteten Token abzüglich der dorthin gebrannten Token entspricht: eine integrierte Prüfung des Gesamtangebots. Behalte die Rohbeträge als Ganzzahlen bis zur abschließenden Division bei; werden die einzelnen Zeilen zuerst in Dezimalzahlen umgewandelt, summieren sich Rundungsfehler. Verwende einen breiten Dezimaldatentyp, da Rohbeträge 64-Bit-Ganzzahlen überschreiten können.
Events und balanceOf können aus bekannten Gründen voneinander abweichen:
- Rebasing-Token verändern den Saldo aller Halter durch einen globalen Multiplikator, ohne für jeden Halter ein eigenes
Transfer-Event auszugeben. Die aus Events berechneten Summen sind daher sofort veraltet. - Verzinsliche Token berechnen
balanceOfanhand eines wachsenden Index; Events erfassen Bewegungen des Kapitals, nicht die aufgelaufenen Zinsen. - Token mit Transfergebühren schreiben dem Empfänger weniger gut, als der Absender sendet. Die meisten geben Events über die Nettobeträge aus, manche aber über den Bruttobetrag.
- Nicht standardkonforme Contracts, meist ältere, minten ohne
Transfer-Event; auch Freeze-and-Burn-Funktionen von Emittenten können statt eines solchen Events ein benutzerdefiniertes Event ausgeben.
Bei diesen Token ist der Zustand maßgeblich, und Events erzählen nur die Geschichte expliziter Transfers. Die Lektion Token-Transfers lesen behandelt sie ausführlich.
Snapshots auf einer Blockhöhe
Ein Saldo „am 31. März“ ist mehrdeutig, solange er nicht einem Block zugeordnet ist. Wandle den Zeitpunkt in den letzten Block um, dessen Zeitstempel am oder vor dem Stichtag liegt, und verwende UTC. Notiere sowohl die Höhe als auch den Block-Hash. Auf Ethereum akzeptieren Zustandsmethoden wie eth_getBalance und eth_call eine Blocknummer oder – gemäß EIP-1898 – einen Block-Hash. So ist garantiert, dass die Antwort von genau diesem Block stammt, selbst wenn eine Reorganisation den Block später durch einen anderen auf derselben Höhe ersetzt. Für Abfragen zu alten Blöcken wird ein Node benötigt, der historische Zustände speichert (ein Archiv-Node oder ein entsprechender Anbieter).
Bei Multi-Chain-Snapshots hat jede Chain für denselben Zeitpunkt ihren eigenen Block: Die etwa 10-minütigen Bitcoin-Blöcke und die 12-Sekunden-Slots von Ethereum bieten eine unterschiedliche zeitliche Granularität, und ein L2-Block eines Rollups kann Einzahlungen widerspiegeln, die dessen L1 gerade erst aufgenommen hat. Nenne im Bericht den für jede Chain gewählten Block.
Abstimmungsprüfungen
Eine Rekonstruktion ist erst abgeschlossen, wenn sie geprüft wurde. Nützliche Tests:
- Abgeleiteter Saldo im Vergleich zum Zustand: Vergleiche die abgeleiteten Salden aller erfassten Adressen mit dem Zustand des Nodes oder bei großen Mengen mit einer Zufallsstichprobe auf mehreren Blöcken.
- Erhaltung: Die Summe aller abgeleiteten Token-Salden ohne die Nulladresse sollte auf demselben Block
totalSupply()entsprechen; bei einer UTXO-Chain darf die Summe des UTXO-Satzes die gesamte Ausgabe nicht überschreiten. - Keine negativen Salden: Ein negativer laufender Saldo bedeutet, dass eine Gutschrift fehlt, meist ein interner Aufruf, ein Mint ohne Event oder eine Auszahlung.
- Grenzwerte: Der Saldo vor der ersten Aktivität einer Adresse beträgt null, und der Saldo nach einer bekannten vollständigen Übertragung entspricht dem, was nach dieser Übertragung zurückblieb.
- Abweichungen erklären: Lege im Voraus eine Toleranz fest (zum Beispiel null für native Salden, einen angegebenen Betrag für Rebasing-Token) und dokumentiere die Ursache jeder Abweichung.
| Symptom | Wahrscheinliche Ursache |
|---|---|
| Abgeleiteter Saldo liegt um Beträge zu niedrig, die Contract-Auszahlungen entsprechen | Interne Aufrufe aus Traces fehlen |
| Abgeleiteter Saldo liegt um kleine Beträge zu hoch | Gasgebühren fehlgeschlagener Transaktionen nicht abgezogen |
| Kleine, regelmäßige, unerklärte Gutschriften | Auszahlungen auf der Konsensschicht oder Gebührenempfänger-Prämien |
Token-Salden der Halter weichen täglich von balanceOf ab | Rebasing- oder verzinslicher Token |
Summe der Salden weicht von totalSupply() ab | Mints oder Burns ohne Transfer-Events |
Prüfungsschwerpunkt: der Unterschied zwischen der Ableitung eines Saldos aus dem Verlauf und dem Auslesen aus dem Zustand und warum Berichte beides benötigen; Berechnung eines UTXO-Saldos auf einer Höhe und die Gegenprobe mit empfangenen minus gesendeten Beträgen; Bestandteile eines nativen Kontosaldos, insbesondere interne Aufrufe, Auszahlungen und Gasgebühren fehlgeschlagener Transaktionen; Berechnung der Gasgebühren als verbrauchtes Gas × effektiver Gaspreis; Ableitung von Token-Salden aus Transfer-Events und die Angebotsprüfung anhand der Nulladresse; warum Rebasing-Token, verzinsliche Token und Token mit Transfergebühren Event-Summen verfälschen; Snapshots an eine Blockhöhe und einen Block-Hash binden (EIP-1898); übliche Abstimmungsprüfungen und was die jeweilige Art der Abweichung vermuten lässt.