Dati on-chain e SQL · Lezione 6 di 9
Qualità, normalizzazione e provenienza dei dati
10 min di lettura
La maggior parte dei valori errati nell'analisi blockchain non deriva da euristiche inadeguate, ma da comuni errori nei dati: un importo errato di un fattore 10^12, un confine giornaliero nel fuso orario sbagliato, un trasferimento conteggiato due volte, una transazione Polygon inclusa nei totali di Ethereum. Questi errori sono facili da commettere e difficili da individuare in un grafico definitivo. Questa lezione illustra le regole di normalizzazione che li prevengono e la registrazione della provenienza che consente a chiunque di verificare il lavoro svolto.
Unità: mantenere gli interi fino alla fine
Le blockchain memorizzano gli importi come numeri interi espressi nell'unità minima. I separatori decimali sono una convenzione di visualizzazione.
| Asset | Unità di base | Conversione |
|---|---|---|
| Bitcoin | satoshi | 1 BTC = 100.000.000 sat (10^8) |
| Ether | wei | 1 ETH = 10^18 wei; 1 gwei = 10^9 wei |
| Token ERC-20 | unità grezze | Dividere per 10^decimals, leggendo il valore dal contratto del token |
I decimali dei token variano: 18 è un valore comune, ma USD Coin ne utilizza 6 e Wrapped Bitcoin 8. Il valore decimals è facoltativo nello standard ERC-20 ed è puramente informativo: i saldi del contratto sono sempre interi grezzi. Occorre leggerlo da ciascun contratto su ciascuna blockchain, anziché presumerlo, perché una versione di un token trasferita tramite bridge può differire dall'originale. Il costo di un'ipotesi errata è elevato: un importo grezzo di 2.500.000.000 corrisponde a 2.500 token con 6 decimali, ma a 0,0000000025 con 18 decimali, una differenza di un fattore 10^12.
Ne derivano due insidie. La prima è la virgola mobile. Bitcoin Core restituisce gli importi in BTC come numeri JSON, ad esempio 0.00485900, e un parser implementato senza le dovute precauzioni li trasforma in valori binari in virgola mobile che non possono rappresentare esattamente la maggior parte delle frazioni decimali; di conseguenza, le somme presentano piccoli scostamenti e le riconciliazioni non riescono. Interpretare gli importi come stringhe decimali e convertirli immediatamente in satoshi interi. La seconda è l'overflow. Gli importi di Ethereum sono interi senza segno a 256 bit, fino a circa 1,16 × 10^77. Un BIGINT con segno a 64 bit raggiunge al massimo 9.223.372.036.854.775.807, che in wei corrispondono a soli 9,22 ETH circa, e nemmeno un DECIMAL a 38 cifre può contenere ogni valore possibile. Memorizzare gli importi grezzi come testo o in un tipo numerico ad ampia capacità e convertirli nell'ultimo passaggio:
-- Gli importi grezzi restano esatti; la conversione avviene solo per la visualizzazione
SELECT tt.transaction_hash,
tt.log_index,
tt.amount_raw, -- intero esatto conservato come testo
CAST(tt.amount_raw AS DECIMAL(38, 0))
/ POWER(10, tk.decimals) AS amount_display -- approssimativo
FROM token_transfers tt
JOIN tokens tk
ON tk.chain_id = tt.chain_id
AND tk.token_address = tt.token_address;
Il join su entrambi i campi, chain_id e indirizzo del contratto, seleziona il valore decimals corretto per ciascun token su ciascuna blockchain. La conversione di tipo non riesce per i valori che superano le 38 cifre, segnalando così i token spam con quantità in circolazione assurde anziché alterare silenziosamente i totali; alcuni motori offrono invece decimali a 76 cifre o tipi nativi a 256 bit. POWER restituisce un numero in virgola mobile nella maggior parte dei motori, quindi amount_display è un'approssimazione, accettabile solo per la visualizzazione; le somme e le riconciliazioni utilizzano amount_raw. Non dividere invece per un DECIMAL(38, 0): alcuni motori assegnano al risultato la scala degli operandi, che qui è zero, e lo troncano silenziosamente a un numero intero.
Tempo: UTC e orario del blocco
I timestamp dei blocchi sono espressi in tempo Unix: secondi trascorsi dal 1° gennaio 1970, UTC. Mantenere ogni timestamp in UTC, raggruppare giorni e mesi in UTC e dichiararlo nella relazione. La conversione in un fuso orario locale sposta le transazioni oltre i confini giornalieri e i cambiamenti dovuti all'ora legale creano ore che si verificano due volte o non si verificano affatto. Una transazione inclusa in un blocco alle 23:30 UTC del 31 marzo appartiene a marzo in una relazione basata su UTC, ma al 1° aprile per un analista i cui strumenti visualizzano UTC+2 senza segnalarlo; se due gruppi confrontano totali mensili calcolati in questo modo, otterranno risultati diversi senza accorgersi del motivo. Memorizzare i timestamp come valori UTC (o secondi Unix grezzi) e convertirli solo nel livello di presentazione, indicando il fuso utilizzato.
Occorre ricordare cosa rappresenta il timestamp. È l'orario del blocco, non il momento della firma o della trasmissione, e su Bitcoin può essere precedente a quello del blocco precedente. Ordinare gli eventi per altezza (e posizione all'interno del blocco), non per timestamp. Quando si mettono in corrispondenza attività su blockchain diverse, tenere conto dei differenti intervalli tra i blocchi e delle diverse regole sui timestamp.
Identificativi e relativi formati
Lo stesso identificativo può essere scritto in diversi modi e i join non producono corrispondenze, senza segnalarlo, quando i formati differiscono.
- Gli indirizzi EVM appaiono nei wallet e negli explorer nella forma con checksum EIP-55, che combina maiuscole e minuscole (
0xA1b2…9F), ma sono in minuscolo nei log e nella maggior parte dei dataset. Convertire in minuscolo tutti gli indirizzi e gli hash esadecimali per la memorizzazione e i join; mantenere la forma con checksum solo per la visualizzazione. - Valori con riempimento: un indirizzo in un topic di log occupa 32 byte; l'indirizzo è costituito dagli ultimi 20.
- I txid Bitcoin sono visualizzati con un ordine dei byte inverso rispetto a quello interno; memorizzare la forma visualizzata.
- Indirizzi Bitcoin: gli indirizzi bech32 e bech32m (
bc1…) non distinguono tra maiuscole e minuscole e sono convenzionalmente in minuscolo, mentre gli indirizzi Base58 (1…,3…) distinguono tra maiuscole e minuscole. Convertire in minuscolo un indirizzo Base58 lo rende inutilizzabile. - Le quantità esadecimali provenienti da Ethereum JSON-RPC (
"0x1406f52") devono essere interpretate in base 16; leggerle come stringhe decimali o eliminare il prefisso produce risultati privi di senso.
Identificativi della blockchain
Lo stesso indirizzo può esistere su decine di blockchain EVM, controllato dalla stessa chiave ma con saldi indipendenti, e gli hash delle transazioni hanno ovunque un formato identico. Ogni riga necessita di un identificativo della blockchain. Le blockchain EVM utilizzano il chain ID, introdotto da EIP-155 per la protezione dagli attacchi di replay e restituito da eth_chainId:
| Blockchain | Chain ID |
|---|---|
| Ethereum mainnet | 1 |
| OP Mainnet | 10 |
| BNB Smart Chain | 56 |
| Gnosis | 100 |
| Polygon PoS | 137 |
| Base | 8453 |
| Arbitrum One | 42161 |
| Avalanche C-Chain | 43114 |
| Sepolia (testnet) | 11155111 |
Un registro dei chain ID mantenuto dalla comunità è il riferimento abituale per le altre blockchain. Utilizzare eth_chainId anziché net_version, il cui identificativo di rete può differire dal chain ID. Escludere esplicitamente le testnet e trattare i token trasferiti tramite bridge come asset distinti, con i propri indirizzi di contratto su ciascuna blockchain; la lezione Bridge, layer 2, sidechain e altre architetture blockchain tratta la mappatura dei flussi tra di essi. Bitcoin non ha un chain ID, ma le sue reti di test utilizzano prefissi degli indirizzi diversi (tb1… anziché bc1…).
Deduplicazione e chiavi naturali
I duplicati si introducono attraverso caricamenti retrospettivi sovrapposti, richieste ripetute, riorganizzazioni (la stessa transazione registrata in due blocchi) ed esportazioni unite. Definire una chiave naturale per ciascun tipo di record consente di rilevarli:
| Record | Chiave naturale |
|---|---|
| Blocco | block_hash |
| Transazione Bitcoin | block_hash, txid |
| Output Bitcoin | txid, vout |
| Transazione EVM | chain_id, hash |
| Log EVM | chain_id, block_hash, log_index |
| Traccia EVM | chain_id, transaction_hash, trace_address |
Perché una transazione Bitcoin necessita dell'hash del blocco? Perché l'unicità dei txid non è garantita: due delle prime transazioni coinbase sono state ripetute esattamente nei blocchi 91.842 e 91.880; BIP 30 le ha ammesse come eccezioni pregresse e BIP 34 ha successivamente reso impossibili tali ripetizioni imponendo l'altezza del blocco in ogni coinbase. La query seguente mantiene una sola copia di ciascun log e soltanto i log dei blocchi canonici:
-- Una riga per log, solo dai blocchi canonici
WITH ranked AS (
SELECT l.*,
ROW_NUMBER() OVER (
PARTITION BY l.chain_id, l.block_hash, l.log_index
ORDER BY l.ingested_at DESC) AS rn
FROM raw_logs l
)
SELECT *
FROM ranked
WHERE rn = 1
AND block_hash IN (SELECT block_hash FROM canonical_blocks);
ROW_NUMBER numera le copie di ciascuna chiave e mantiene quella acquisita più di recente; il filtro finale elimina i log dei blocchi sostituiti da una riorganizzazione. Anche i duplicati logici sono rilevanti: conteggiare un log Transfer e la traccia della stessa chiamata al token, oppure ETH nativo insieme a WETH, comporta un doppio conteggio del valore.
Registrazione della provenienza
La provenienza è la registrazione dell'origine di un dataset e del modo in cui è stato prodotto. Allegarne una a ogni estrazione che supporta una risultanza:
{
"dataset": "token_transfers_to_target",
"chain_id": 1,
"block_range": [21000000, 21000999],
"anchor_block_hash": "0x5e1c…a7d0",
"finality": "finalized",
"source": "own archive node",
"client_version": "Geth/v1.16.x",
"method": "eth_getLogs",
"query_file": "queries/transfers_to_target.sql",
"retrieved_at_utc": "2026-10-28T14:05:00Z",
"row_count": 37,
"output_sha256": "b94d…e9f1"
}
L'intervallo dei blocchi e l'hash di ancoraggio fissano i dati; lo stato di finalità mostra che non possono cambiare; la fonte, la versione del client e il metodo identificano come sono stati ottenuti; il file della query documenta la trasformazione; l'orario di acquisizione in UTC li data; il numero di righe e l'hash SHA-256 del file di output consentono a chiunque di confermare di essere in possesso degli stessi dati. Un revisore dotato di un nodo può rieseguire la query e confrontare i risultati. La lezione Strumenti, riproducibilità e validazione estende questo approccio alle query sottoposte a controllo di versione in un data warehouse.
Errori comuni nei dati
| Errore | Sintomo | Prevenzione |
|---|---|---|
| Importi in virgola mobile | Totali errati nelle ultime cifre | Unità di base intere, interpretazione decimale |
| Decimali errati | Importi errati di un fattore 10^12 (6 trattato come 18) | Leggere decimals per ciascun contratto e blockchain |
| Fuso orario locale | Totali giornalieri spostati tra giorni diversi | UTC in tutte le fasi |
| Chain ID mancante | Attività di sidechain nei totali della mainnet | chain_id su ogni riga e in ogni join |
| Transazioni non riuscite conteggiate | Valore che non è mai stato trasferito | Filtrare in base a status = 1 e agli errori delle tracce |
| Righe di blocchi riorganizzati mantenute | Transazioni assenti dalla blockchain definitiva | Eseguire un join con i blocchi canonici tramite hash |
| Identificativi alterati nella distinzione tra maiuscole e minuscole | I join non trovano alcune righe; indirizzi Base58 non validi | Convertire in minuscolo solo i valori esadecimali |
Un controllo finale poco oneroso rileva molti di questi problemi contemporaneamente: scegliere da tre a cinque righe tra le più rilevanti per una risultanza e verificarle una per una rispetto a un nodo o a una seconda fonte, confrontando hash del blocco, importo in unità di base e timestamp. Se anche una sola riga non coincide, fermarsi e individuarne il motivo prima di fidarsi del dato aggregato.
Focus sull'esame: satoshi, wei, gwei e decimali dei token, e perché gli importi restano interi; rischi della virgola mobile e dell'overflow a 64 bit (BIGINT contiene solo circa 9,22 ETH in wei); timestamp UTC e ordinamento per altezza; checksum EIP-55 rispetto alla memorizzazione in minuscolo e distinzione tra maiuscole e minuscole in Base58; chain ID (EIP-155, eth_chainId) e perché ogni riga ne necessita; chiavi naturali per la deduplicazione e txid duplicati di BIP 30; campi di una registrazione della provenienza; errori comuni nei dati e come prevenirli.