Modelli UTXO e basati su account · Lezione 4 di 6
Ricostruire saldi e cronologie
9 min di lettura
«Quanto deteneva questo indirizzo in quella data e come è arrivato a quel saldo?» È una delle domande più comuni che vengono poste a un analista, che si tratti di una revisione del rischio di un exchange, di una relazione di un esperto nominato dal tribunale o di un’inchiesta giornalistica. Ci sono due modi per rispondere: ricavare il saldo dalla cronologia o leggerlo dallo stato della blockchain. Un analista scrupoloso usa entrambi e spiega qualsiasi differenza. Questa lezione mostra come farlo per le monete UTXO, i saldi nativi degli account e i token.
Due metodi per ricavare un saldo
| Metodo | Come | Punti di forza | Punti deboli |
|---|---|---|---|
| Ricavarlo dalla cronologia | Sommare ogni accredito e addebito fino a un blocco | Spiega ogni movimento; funziona in un data warehouse | È facile tralasciare una categoria di movimenti |
| Leggerlo dallo stato | Chiedere a un nodo il saldo a un blocco | È autorevole per quella blockchain e quel blocco | Non fornisce spiegazioni; per i blocchi più vecchi serve lo stato d’archivio |
Il dato ricavato dalla cronologia è quello che entra in una relazione, perché racconta la storia. Il dato dello stato serve come verifica. Quando coincidono, la cronologia è completa; quando differiscono, manca qualcosa, e l’entità dello scarto di solito indica cosa.
Saldi UTXO
Su una blockchain UTXO, il saldo di un insieme di script all’altezza H è la somma degli output verso quegli script, creati all’altezza H o prima e non spesi all’altezza H o prima (vedi la lezione Il modello UTXO per gli analisti). Ecco un esempio svolto per un indirizzo:
| Outpoint | Creato all’altezza | Importo (BTC) | Speso all’altezza |
|---|---|---|---|
4f3a…c21e:1 | 880.100 | 0,4000 | 880.900 |
a91c…3b07:0 | 880.450 | 1,2500 | non speso |
77d2…e4a0:3 | 881.020 | 0,0800 | 885.300 |
c0b5…91f2:0 | 884.700 | 0,3000 | non speso |
All’altezza 882.000, gli output non spesi sono a91c…3b07:0 e 77d2…e4a0:3: 1,33 BTC. All’altezza 886.000, sono a91c…3b07:0 e c0b5…91f2:0: 1,55 BTC. Il totale ricevuto è 2,03 BTC e il totale inviato è 0,48 BTC; sottraendo il totale inviato da quello ricevuto all’altezza 886.000 si ottiene lo stesso saldo, 1,55 BTC: un utile controllo interno.
Aspetti da tenere presenti:
- Un wallet non è un indirizzo. Le disponibilità di una persona si estendono su molti indirizzi e spesso su diversi tipi di script. Se si conoscono i descrittori di output o la chiave pubblica estesa di un wallet (per esempio, se forniti dal proprietario in una controversia), ricavane tutti gli indirizzi, compresi quelli di resto, prima di sommarli.
- Dati non confermati e riorganizzati. Le transazioni nel mempool non fanno parte dei saldi. Crea uno snapshot a un’altezza sufficientemente profonda da essere stabile (vedi la lezione Blocchi e header come dati).
- Gli output coinbase con meno di 100 blocchi esistono, ma non possono ancora essere spesi; specificalo se è in discussione il saldo di un miner.
- Le verifiche dello stato sono limitate:
scantxoutsetdi Bitcoin Core vede solo l’insieme UTXO corrente, quindi per le verifiche storiche serve un indicizzatore o un secondo dataset creato in modo indipendente.
Saldi nativi sulle blockchain basate su account
Per ETH e altre valute native, un saldo al blocco N si scompone in categorie che risiedono in dataset diversi:
saldo(N) = valore ricevuto in transazioni di primo livello riuscite
+ valore ricevuto in chiamate interne riuscite (tracce)
+ prelievi del livello di consenso e ricompense per il destinatario delle commissioni
- valore inviato in transazioni di primo livello e chiamate interne riuscite
- commissioni gas pagate per ogni transazione inviata, riuscita o non riuscita
(Le allocazioni della genesi e le ricompense di mining precedenti al Merge aggiungono termini per gli indirizzi più vecchi.) Ecco un esempio svolto per 0xA1b2…9F al blocco N:
| Voce | Fonte | Importo (ETH) |
|---|---|---|
| Deposito da un exchange | Transazione | +5,000000 |
| Pagamento da un contratto | Traccia (chiamata interna) | +1,000000 |
| Prelievi del validatore | Elenco dei prelievi del blocco | +0,032000 |
| Pagamento inviato | Transazione | −2,500000 |
| Gas per quel pagamento: 21.000 × 20 gwei | Ricevuta | −0,000420 |
| Transazione non riuscita: valore non inviato | status della ricevuta = 0 | 0 |
| Gas per la transazione non riuscita: 70.000 × 30 gwei | Ricevuta | −0,002100 |
| Saldo ricavato | 3,529480 |
Se eth_getBalance(0xA1b2…9F, N) restituisce 3,529480 ETH, la cronologia è completa. Gli errori tipici lasciano tracce nello scarto. Ignorare le tracce fa risultare il saldo inferiore esattamente dell’accredito interno (1,0). Contare il valore della transazione non riuscita fa risultare il saldo inferiore del suo valore; dimenticare il relativo gas lo fa risultare superiore di 0,0021. La mancanza dei prelievi produce uno scarto costituito da importi piccoli e regolari. Il gas si calcola moltiplicando il gas usato per il prezzo effettivo del gas indicato nella ricevuta, non per il limite di gas; alle transazioni di tipo 3 si aggiungono le commissioni blob.
Saldi dei token: eventi e balanceOf
I saldi dei token risiedono nello storage del contratto del token. Si possono ricavare sommando gli eventi Transfer oppure leggere con balanceOf(holder) tramite eth_call al blocco N. La versione ricavata tramite SQL, usando una tabella delle transazioni decodificate, è la seguente:
-- Saldi dei token al blocco 21.000.000, ricavati dagli eventi 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 -- il token ha 6 decimali
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Ogni trasferimento diventa due righe: un accredito al destinatario e un addebito al mittente. La somma per titolare corrisponde al saldo. Le nuove emissioni compaiono come trasferimenti dall’indirizzo zero, che finisce quindi con un saldo negativo pari a tutto ciò che è stato emesso meno tutto ciò che è stato bruciato: un controllo integrato dell’offerta. Mantieni gli importi interi grezzi fino alla divisione finale; convertire prima ogni riga in decimali accumula errori di arrotondamento. Usa un tipo decimale con ampia precisione, perché gli importi grezzi possono superare gli interi a 64 bit.
Gli eventi e balanceOf non coincidono per ragioni note:
- I token rebase modificano il saldo di ogni titolare tramite un moltiplicatore globale, senza eventi
Transferper ciascun titolare. Le somme degli eventi diventano subito obsolete. - I token fruttiferi calcolano
balanceOfusando un indice crescente; gli eventi registrano i movimenti del capitale, non gli interessi maturati. - I token con commissione sul trasferimento accreditano al destinatario meno di quanto inviato dal mittente; la maggior parte emette eventi per gli importi netti, ma alcuni emettono eventi per l’importo lordo.
- I contratti non standard, soprattutto quelli più vecchi, emettono nuova valuta senza generare eventi
Transfer; oppure le funzioni dell’emittente per congelare e bruciare token generano un evento personalizzato.
Per questi token, lo stato è la fonte attendibile e gli eventi raccontano solo la storia dei trasferimenti espliciti; la lezione Leggere i trasferimenti dei token li tratta in dettaglio.
Snapshot a un’altezza di blocco
Un saldo «al 31 marzo» è ambiguo finché non viene associato a un blocco. Converti l’ora nell’ultimo blocco il cui timestamp è pari o precedente all’ora limite, in UTC, e registra sia l’altezza sia l’hash del blocco. Su Ethereum, i metodi di lettura dello stato come eth_getBalance e eth_call accettano un numero di blocco oppure, in base a EIP-1898, un hash del blocco, che garantisce che la risposta provenga da quel blocco esatto anche se una riorganizzazione lo sostituisce in seguito a quell’altezza. Per interrogare i blocchi più vecchi serve un nodo che conservi lo stato storico (un nodo archivio o un provider).
Per gli snapshot multi-chain, ogni blockchain ha il proprio blocco per lo stesso istante: i blocchi di Bitcoin, che si producono all’incirca ogni 10 minuti, e gli slot di Ethereum, di 12 secondi, avranno granularità diverse; un blocco L2 di un rollup può riflettere depositi che la sua L1 ha appena incluso. Nel rapporto, indica il blocco scelto per ciascuna blockchain.
Verifiche di riconciliazione
Una ricostruzione non è completa finché non è stata verificata. Ecco alcuni test utili:
- Saldo ricavato dalla cronologia contro stato: confronta i saldi ricavati con lo stato del nodo per ogni indirizzo incluso nel perimetro, oppure, per gli insiemi numerosi, su un campione casuale a diversi blocchi.
- Conservazione: la somma di tutti i saldi dei token ricavati, escludendo l’indirizzo zero, dovrebbe essere uguale a
totalSupply()allo stesso blocco; su una blockchain UTXO, il totale dell’insieme UTXO non dovrebbe superare l’emissione totale. - Nessun saldo negativo: un saldo progressivo negativo indica un accredito mancante, il più delle volte una chiamata interna, una nuova emissione senza evento o un prelievo.
- Limiti: il saldo prima della prima attività di un indirizzo è zero; il saldo dopo uno sweep noto corrisponde a ciò che lo sweep ha lasciato.
- Scarti spiegati: stabilisci in anticipo una tolleranza (per esempio, zero per i saldi nativi e un importo dichiarato per i token rebase) e documenta la causa di ogni differenza.
| Sintomo | Causa probabile |
|---|---|
| Saldo ricavato inferiore di importi che corrispondono ai pagamenti dei contratti | Mancano le chiamate interne delle tracce |
| Saldo ricavato superiore di piccoli importi | Non sono state detratte le commissioni gas delle transazioni non riuscite |
| Piccoli accrediti regolari non spiegati | Prelievi del livello di consenso o ricompense per il destinatario delle commissioni |
I saldi dei titolari dei token si discostano ogni giorno da balanceOf | Token rebase o fruttifero |
La somma dei saldi differisce da totalSupply() | Nuove emissioni o token bruciati senza eventi Transfer |
Focus sull'esame: la differenza tra ricavare un saldo dalla cronologia e leggerlo dallo stato, e perché i rapporti richiedono entrambi; il calcolo di un saldo UTXO a una determinata altezza e la verifica tramite ricevuto meno inviato; le componenti di un saldo nativo di un account, soprattutto chiamate interne, prelievi e gas sulle transazioni non riuscite; il calcolo del gas come gas usato × prezzo effettivo del gas; il ricavo dei saldi dei token dagli eventi Transfer e la verifica dell’offerta tramite l’indirizzo zero; perché i token rebase, fruttiferi e con commissione sul trasferimento invalidano le somme degli eventi; l’associazione degli snapshot a un’altezza di blocco e a un hash (EIP-1898); le verifiche standard di riconciliazione e cosa suggerisce ciascun tipo di scarto.Domande di esercitazione
Verifica la tua comprensione. Queste domande non contano per l’esame.
Accedi per salvare i tuoi progressi e riprendere da dove hai lasciato. È gratuito. Accedi