Dati on-chain e SQL · Lezione 5 di 9
Il modello a conti e la ricostruzione dei saldi
15 min di lettura
Ethereum e le numerose blockchain che ne riprendono la struttura mantengono un saldo aggiornato per ogni indirizzo anziché un insieme di monete distinte. Ciò modifica ciò che è possibile tracciare, il significato di «transazione» e l'identità del soggetto che agisce realmente: con gli smart account, l'indirizzo che firma, quello che paga il gas e quello nel campo from corrispondono spesso a tre soggetti diversi, e confonderli porta ad attribuire l'attività al soggetto sbagliato. Questo modello incide anche su una domanda frequente nelle indagini, «quanto deteneva questo indirizzo a quella data e come è arrivato a quel saldo?», alla quale un esaminatore scrupoloso risponde due volte: ricavando il saldo dalla cronologia e leggendolo dallo stato della blockchain, per poi spiegare eventuali differenze.
Stato, non monete
Una blockchain con modello a conti memorizza uno stato globale: una mappatura da ciascun indirizzo di 20 byte a un record di conto. Su Ethereum tale record contiene quattro campi:
| Campo | Significato | Utilizzo per l'analista |
|---|---|---|
nonce | Transazioni inviate (EOA) o contratti creati (contratto) | Conteggio delle attività, ordinamento, verifiche sugli indirizzi nuovi |
balance | Valuta nativa, in wei | Disponibilità a un determinato blocco |
codeHash | Hash del codice del conto (vuoto per un EOA semplice) | Distinguere EOA, contratti ed EOA delegati |
storageRoot | Radice dello spazio di archiviazione chiave-valore del conto | Dove risiedono i saldi dei token e i dati dei contratti |
Ogni blocco vincola crittograficamente lo stato risultante tramite lo stateRoot nella propria intestazione. I saldi si leggono con eth_getBalance(address, block), lo spazio di archiviazione dei contratti tramite eth_call (ad esempio il balanceOf di un token) o eth_getStorageAt.
| Domanda | Blockchain UTXO | Blockchain a conti |
|---|---|---|
| Unità di valore | Output distinti, spesi per intero | Un saldo per indirizzo |
| Resto | Un nuovo output, spesso verso un nuovo indirizzo | Nessuno: la parte residua rimane nel saldo |
| Collegamento tra un pagamento in uscita e uno in entrata | Esatto per ogni output; ambiguo all'interno di una transazione con più input | Mai esatto: tutti i depositi confluiscono nello stesso saldo |
| Riutilizzo degli indirizzi | Sconsigliato | Normale; un solo indirizzo per anni |
| Protezione dal replay | Ogni output può essere speso una sola volta | Ogni transazione deve usare il nonce successivo |
Quando 10 ETH arrivano a un indirizzo che detiene già 50 ETH, nulla nei dati indica quali ETH siano usciti successivamente. Qualsiasi attribuzione di un pagamento in uscita a uno specifico deposito è una convenzione di tracciamento scelta dall'analista, non un fatto registrato (la lezione 3.3 illustra i metodi).
EOA, contratti ed EOA delegati
Un conto di proprietà esterna (EOA) è controllato da una chiave privata ed è l'unico tipo di conto che può originare una transazione. Un conto contratto dispone di codice e spazio di archiviazione e agisce solo quando viene chiamato. eth_getCode(address, block) restituisce 0x per un EOA e il bytecode distribuito per un contratto. Tre insidie rendono questa verifica meno semplice di quanto sembri:
- Momento della verifica. Il codice esiste solo a partire dal blocco in cui il contratto è stato creato. Gli indirizzi possono essere calcolati prima della distribuzione (CREATE2 rende questa possibilità intenzionale), quindi un indirizzo privo di codice al blocco N può essere un contratto a N+1. Verificare il codice al blocco rilevante.
- Distruzione. Prima dell'aggiornamento Dencun (marzo 2024),
SELFDESTRUCTeliminava il codice di un contratto. Con EIP-6780 lo fa solo quando viene chiamato nella stessa transazione che ha creato il contratto; altrimenti si limita a trasferire altrove il saldo. - Delega. Dall'aggiornamento Pectra (maggio 2025), un EOA può contenere il designatore di delega EIP-7702 di 23 byte, costituito da
0xef0100seguito da un indirizzo delegato. Una regola ingenua del tipo «ha codice, quindi è un contratto» lo classifica erroneamente: il conto è ancora controllato dalla propria chiave privata.
Un classificatore robusto restituisce quindi tre classi (EOA, contratto, EOA delegato) riferite a un determinato blocco.
Il nonce è una risorsa preziosa per gli analisti. Per un EOA equivale al numero di transazioni inviate, quindi nonce 0 significa che l'indirizzo non ha mai inviato nulla, e lacune o raffiche di attività rivelano schemi operativi (lezione 4.3). Due precisazioni: anche ogni autorizzazione EIP-7702 firmata da un EOA ne incrementa il nonce, e il nonce di un contratto conta le creazioni di contratti, partendo da 1 secondo EIP-161. Gli indirizzi dei contratti derivano dal soggetto che li distribuisce: CREATE usa l'indirizzo e il nonce di tale soggetto, CREATE2 usa il soggetto che distribuisce il contratto, un salt e l'hash del codice di creazione; per questo i collegamenti con chi distribuisce un contratto costituiscono elementi probatori solidi.
Dove risiede la cronologia
Una query sullo stato risponde alla domanda «che cosa deteneva questo indirizzo al blocco N?». La cronologia risponde alla domanda «come è arrivato a quel saldo?» ed è distribuita su diversi insiemi di dati:
- transazioni di primo livello: valore inviato, più gas pagato dal mittente;
- tracce delle chiamate interne: valore movimentato dai contratti;
- log: trasferimenti di token e altri eventi;
- variazioni di saldo che non sono transazioni: commissioni di priorità accreditate al destinatario delle commissioni del blocco, prelievi del livello di consenso accreditati direttamente agli indirizzi dall'aprile 2023 (EIP-4895: nessun gas, nessuna esecuzione di codice, nessun hash di transazione), allocazioni di genesi e, prima del settembre 2022, ricompense di mining.
I nodi completi conservano solo lo stato recente, quindi lo stato relativo ai blocchi più vecchi richiede un nodo archivio o un fornitore che ne metta a disposizione uno (lezione 2.1).
Transazioni interne
Nel protocollo, una transazione è soltanto il messaggio firmato di primo livello. Quando un contratto inoltra ETH durante l'esecuzione, si tratta di una chiamata di messaggio: nessun hash, nessuna ricevuta e nessun log, a meno che il codice non ne emetta uno. Gli explorer le chiamano «transazioni interne», un'etichetta comoda ma fuorviante. Si supponga che la transazione di un utente invii 0 ETH a un contratto wallet 0x7c3E…12, che durante l'esecuzione chiama 0xD4e5…3a con valore 0xde0b6b3a7640000, pari a 10^18 wei o 1 ETH. L'elenco delle transazioni di 0xD4e5…3a non mostra nulla, eppure il suo saldo è aumentato di 1 ETH. Solo una traccia (lezione 2.4) spiega la variazione. Le sottochiamate annullate non movimentano nulla e devono essere escluse.
Un trasferimento ERC-20 è ancora diverso: una modifica nello spazio di archiviazione del contratto del token, annunciata da un evento Transfer, non una transazione interna (lezione 3.5).
Smart account: chi firma, chi paga, chi agisce
Uno smart account è un wallet implementato come contratto, con regole quali firmatari multipli, limiti di spesa, recupero o gas sponsorizzato.
I wallet a contratto (i wallet multifirma sono il caso classico) detengono fondi presso un indirizzo di contratto. I proprietari firmano fuori dalla blockchain e uno di loro, oppure un relayer, inoltra la transazione, quindi l'EOA in from può essere semplicemente l'ultimo firmatario.
ERC-4337 introduce l'astrazione dei conti senza modifiche al protocollo. Gli utenti firmano una UserOperation (i campi includono sender, nonce, callData, limiti di gas, paymaster e signature) e la inviano a un mempool separato. Un bundler ne raggruppa molte in un'unica transazione che chiama handleOps su un contratto EntryPoint unico, il quale convalida ed esegue ogni operazione e paga il bundler dal deposito del conto o da quello di un paymaster. Per ogni operazione emette:
UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)
Il from della transazione è il bundler, una transazione può contenere operazioni di utenti non collegati tra loro, la ricevuta riguarda l'intero gruppo e il soggetto che agisce realmente è il sender dell'evento. Utilizzando lo schema generico della lezione 2.7:
-- Una riga per ogni UserOperation ERC-4337, con bundler, smart account e paymaster
SELECT l.block_number,
l.transaction_hash,
t.from_address AS bundler,
'0x' || SUBSTR(l.topic2, 27, 40) AS smart_account, -- sender indicizzato
'0x' || SUBSTR(l.topic3, 27, 40) AS paymaster -- indirizzo zero = nessuno
FROM logs AS l
JOIN transactions AS t ON t.hash = l.transaction_hash
WHERE l.topic0 = '0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f'
AND l.contract_address IN (SELECT address FROM entrypoint_contracts);
topic0 è l'hash Keccak-256 della firma dell'evento e SUBSTR(topic, 27, 40) estrae un indirizzo indicizzato dal relativo topic di 32 byte, completato con zeri (lezione 2.9); alcuni motori chiamano la funzione SUBSTRING e gli insiemi di dati che memorizzano i topic in formato binario richiedono invece l'estrazione di una porzione di byte. Limitare la ricerca agli indirizzi EntryPoint noti (uno per versione, conservati in una piccola tabella del caso) evita di includere eventi simili emessi da altri contratti.
EIP-7702 consente a un EOA esistente di adottare il comportamento di uno smart account. Una transazione di tipo 4 contiene autorizzazioni [chain_id, address, nonce, y_parity, r, s]; ciascuna imposta il codice del firmatario sul designatore di delega, che persiste finché non viene sostituito o rimosso, e un'autorizzazione con chain_id 0 è valida su ogni blockchain. L'indirizzo conserva la propria cronologia e la propria chiave, ma ora può raggruppare chiamate o far pagare il proprio gas ad altri. Anche le destinazioni delle deleghe costituiscono elementi probatori: una dashboard pubblica on-chain, pubblicata dalla società di trading Wintermute nel giugno 2025, ha rilevato che oltre l'80% delle prime deleghe puntava a copie di un unico contratto «sweeper», che svuota i wallet le cui chiavi sono state divulgate.
| Schema | Chi autorizza | from della transazione (paga il gas) | Dove individuare il soggetto che agisce realmente |
|---|---|---|---|
| Transazione EOA semplice | Chiave dell'EOA | Lo stesso EOA | from |
| Wallet a contratto | Firme dei proprietari nei dati della chiamata | Un proprietario o un relayer | to (il wallet) e i suoi eventi |
| Meta-transazione (ERC-2771) | Firma dell'utente | Un relayer tramite un forwarder attendibile | Dati della chiamata decodificati |
| ERC-4337 | Regole di convalida dello smart account | Bundler, rimborsato dal conto o dal paymaster | UserOperationEvent.sender |
| EIP-7702 sponsorizzato | Autorizzazione dell'EOA e payload firmato | Uno sponsor | L'EOA delegato chiamato |
Non equiparare mai chi paga il gas al soggetto che agisce finché non è stato identificato lo schema. Il finanziamento del gas rimane un elemento probatorio utile, ma in presenza di sponsorizzazione può rimandare a un servizio anziché a un proprietario.
Due percorsi per ottenere un saldo
| Percorso | Metodo | 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 | Richiedere a un nodo il saldo a un blocco | Autorevole per quella blockchain e quel blocco | Nessuna spiegazione; i blocchi più vecchi richiedono lo stato archiviato |
Il valore ricavato va nella relazione perché ricostruisce la sequenza degli eventi; il valore letto dallo stato serve come verifica. Quando coincidono, la cronologia è completa. Quando differiscono, manca qualcosa e l'entità dello scarto indica solitamente che cosa.
Saldi UTXO
Su una blockchain UTXO, il saldo di un insieme di script all'altezza H è la somma degli output a essi destinati creati entro H e non spesi entro H (lezione 2.3). Per un singolo indirizzo:
| Outpoint | Creato a | Importo (BTC) | Speso a |
|---|---|---|---|
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 |
A 882.000 gli output non spesi sono a91c…3b07:0 e 77d2…e4a0:3: 1,33 BTC. A 886.000 sono a91c…3b07:0 e c0b5…91f2:0: 1,55 BTC. Il ricevuto (2,03) meno l'inviato (0,48) dà lo stesso risultato, 1,55, un'utile verifica interna. Un wallet non è un indirizzo: se si conoscono i descrittori di output o una chiave pubblica estesa (ad esempio, fornita dal proprietario in una controversia), derivare tutti gli indirizzi, inclusi quelli del resto, prima di sommare. Le transazioni nel mempool non costituiscono saldi; gli output coinbase con meno di 100 blocchi di anzianità esistono, ma non possono ancora essere spesi. scantxoutset di Bitcoin Core vede solo l'insieme UTXO attuale, quindi le verifiche storiche richiedono un indicizzatore o un secondo insieme di dati costruito in modo indipendente.
Saldi nativi sulle blockchain a conti
balance(N) = valore ricevuto in transazioni di primo livello riuscite
+ valore ricevuto in chiamate interne riuscite (tracce)
+ prelievi del livello di consenso e ricompense al destinatario delle commissioni
- valore inviato in transazioni di primo livello e chiamate interne riuscite
- commissioni del gas pagate su ogni transazione inviata, riuscita o fallita
Le allocazioni di genesi e le ricompense di mining precedenti al Merge aggiungono termini per gli indirizzi più vecchi. Un esempio svolto per 0x6F2a…c19E al blocco N:
| Voce | Fonte | Importo (ETH) |
|---|---|---|
| Deposito da un exchange | Transazione | +5,000000 |
| Pagamento da un contratto | Traccia (chiamata interna) | +1,000000 |
| Prelievi dei validatori | Elenco dei prelievi del blocco | +0,032000 |
| Pagamento inviato | Transazione | −2,500000 |
| Gas su quel pagamento: 21.000 × 20 gwei | Ricevuta | −0,000420 |
| Transazione fallita: valore non inviato | status della ricevuta = 0 | 0 |
| Gas sulla transazione fallita: 70.000 × 30 gwei | Ricevuta | −0,002100 |
| Saldo ricavato | 3,529480 |
Se eth_getBalance(0x6F2a…c19E, N) restituisce 3,529480 ETH, la cronologia è completa. Gli errori lasciano segni riconoscibili: ignorare le tracce sottostima il saldo esattamente dell'accredito interno (1,0); conteggiare il valore della transazione fallita lo sottostima di quel valore; dimenticarne il gas lo sovrastima di 0,0021; omettere i prelievi lascia uno scarto costituito da importi piccoli e regolari. Il gas si calcola come gas utilizzato × prezzo effettivo del gas riportato nella ricevuta, mai il limite di gas, più le commissioni blob per le transazioni di tipo 3.
Saldi dei token: eventi e balanceOf a confronto
I saldi dei token risiedono nello spazio di archiviazione del contratto del token. Possono essere ricavati sommando gli eventi Transfer oppure letti con balanceOf(holder) tramite eth_call al blocco N:
-- 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 un accredito e un addebito. Le emissioni sono trasferimenti dall'indirizzo zero, quindi l'indirizzo zero termina con un saldo negativo pari a tutto ciò che è stato emesso meno tutto ciò che è stato bruciato inviandolo a tale indirizzo: una verifica intrinseca dell'offerta. Mantenere gli interi grezzi in un tipo decimale ad ampia capacità fino alla divisione finale.
Gli eventi e balanceOf divergono per ragioni note: i token con rebase modificano ogni saldo tramite un moltiplicatore globale senza un evento per ciascun detentore; i token che maturano interessi calcolano balanceOf a partire da un indice crescente; i token con commissione sul trasferimento accreditano al destinatario meno di quanto il mittente invia, e alcuni emettono l'importo lordo; inoltre, alcuni contratti più vecchi o controllati dall'emittente emettono token senza Transfer oppure emettono un evento personalizzato quando congelano e bruciano token. Per questi token, lo stato è la fonte di verità e gli eventi raccontano soltanto la cronologia dei trasferimenti espliciti.
Istantanee e riconciliazione
Un saldo «al 31 marzo» è ambiguo finché non viene associato a un blocco: scegliere l'ultimo blocco il cui timestamp sia pari o precedente all'istante limite, in UTC, e registrarne altezza e hash. Con EIP-1898, eth_getBalance ed eth_call accettano un hash di blocco, garantendo che la risposta provenga esattamente da quel blocco anche se una riorganizzazione sostituisce successivamente il blocco a quell'altezza. Per istantanee su più blockchain, ciascuna blockchain ha il proprio blocco per lo stesso istante (i blocchi di Bitcoin, di circa 10 minuti, danno una granularità più grossolana rispetto agli slot di 12 secondi di Ethereum, e un blocco di rollup può riflettere depositi che il suo L1 ha appena incluso); indicare il blocco scelto per ciascuna.
Una ricostruzione non è conclusa finché non viene verificata: confrontare i saldi ricavati con lo stato (tutti gli indirizzi nell'ambito dell'analisi, oppure un campione a diversi blocchi); verificare che i saldi dei token ricavati, escluso l'indirizzo zero, siano pari a totalSupply() allo stesso blocco; considerare ogni saldo progressivo negativo come un accredito mancante; verificare le condizioni ai limiti (zero prima della prima attività e quanto lasciato da uno svuotamento noto); e stabilire in anticipo una tolleranza, documentando la causa di ogni residuo.
| Sintomo | Causa probabile |
|---|---|
| Saldo ricavato troppo basso per importi corrispondenti a pagamenti da contratti | Chiamate interne mancanti nelle tracce |
| Saldo ricavato troppo alto per piccoli importi | Gas sulle transazioni fallite non detratto |
| Piccoli accrediti regolari non spiegati | Prelievi del livello di consenso o ricompense al destinatario delle commissioni |
I saldi dei detentori dei token divergono quotidianamente da balanceOf | Token con rebase o che matura interessi |
La somma dei saldi differisce da totalSupply() | Emissioni o distruzioni senza eventi Transfer |
Focus sull'esame: sono previste domande basate su scenari relative a chi ha agito e al motivo per cui un saldo non si riconcilia: i quattro campi del conto e il significato del nonce; perché i saldi aggregati rendono il tracciamento a livello di singola moneta una convenzione; classificazione di EOA, contratti ed EOA delegati EIP-7702 a un dato blocco (0xef0100, EIP-6780); transazioni interne come chiamate di messaggio visibili solo nelle tracce; prelievi e ricompense da commissioni come accrediti non derivanti da transazioni; ruoli ERC-4337 eUserOperationEvent.sender; saldi ricavati e saldi letti dallo stato; componenti del saldo nativo, in particolare il gas sulle transazioni fallite; saldi dei token ricavati dagli eventi e verifica dell'offerta tramite l'indirizzo zero; token che invalidano le somme degli eventi; ancoraggio delle istantanee tramite altezza e hash (EIP-1898); che cosa suggerisce ciascun tipo di scarto.