Modelos UTXO e de contas · Aula 4 de 6
Reconstruindo saldos e históricos
9 min de leitura
“Quanto esse endereço tinha naquela data e como chegou a esse valor?” é uma das perguntas mais comuns feitas a um analista, seja em uma análise de risco de uma exchange, em um relatório de um perito nomeado pelo tribunal ou em uma matéria jornalística. Há duas maneiras de responder: derivar o saldo a partir do histórico ou consultá-lo no estado da blockchain. Um analista cuidadoso faz as duas coisas e explica qualquer diferença. Esta aula mostra como fazer isso para moedas UTXO, saldos nativos de contas e tokens.
Duas abordagens para obter um saldo
| Abordagem | Como funciona | Pontos fortes | Pontos fracos |
|---|---|---|---|
| Derivar do histórico | Somar todos os créditos e débitos até um bloco | Explica cada movimentação; funciona em um data warehouse | É fácil deixar de fora uma categoria de movimentação |
| Consultar no estado | Consultar em um nó o saldo em um bloco | É a fonte de autoridade para aquela blockchain e aquele bloco | Não explica o saldo; blocos antigos exigem estado de arquivo |
O valor derivado é o que entra no relatório, porque conta a história. O valor do estado serve como conferência. Quando ambos coincidem, o histórico está completo; quando diferem, algo está faltando, e o tamanho da diferença geralmente indica o quê.
Saldos UTXO
Em uma blockchain UTXO, o saldo de um conjunto de scripts na altura H é a soma dos outputs destinados a esses scripts, criados na altura H ou antes e não gastos até a altura H (veja a aula O modelo UTXO para analistas). Exemplo prático para um endereço:
| Outpoint | Criado na altura | Valor (BTC) | Gasto na altura |
|---|---|---|---|
4f3a…c21e:1 | 880.100 | 0,4000 | 880.900 |
a91c…3b07:0 | 880.450 | 1,2500 | não gasto |
77d2…e4a0:3 | 881.020 | 0,0800 | 885.300 |
c0b5…91f2:0 | 884.700 | 0,3000 | não gasto |
Na altura 882.000, os outputs não gastos são a91c…3b07:0 e 77d2…e4a0:3: 1,33 BTC. Na altura 886.000, são a91c…3b07:0 e c0b5…91f2:0: 1,55 BTC. O total recebido é 2,03 BTC e o total enviado é 0,48 BTC; subtrair o total enviado do recebido na altura 886.000 resulta no mesmo saldo de 1,55 BTC: uma conferência interna útil.
Pontos de atenção:
- Uma carteira não é um endereço. Os ativos de uma pessoa podem estar distribuídos por muitos endereços e, muitas vezes, por vários tipos de script. Se os descritores de output ou a chave pública estendida de uma carteira forem conhecidos (por exemplo, fornecidos pelo proprietário em uma disputa), derive todos os endereços, inclusive os de troco, antes de somá-los.
- Dados não confirmados e reorganizados. Transações do mempool não são saldos. Faça a captura em uma altura suficientemente profunda para que seja estável (veja a aula Blocos e cabeçalhos como dados).
- Outputs coinbase com menos de 100 blocos existem, mas ainda não podem ser gastos; informe isso se o saldo de um minerador estiver em questão.
- As conferências de estado são limitadas: o
scantxoutsetdo Bitcoin Core consulta apenas o conjunto UTXO atual, portanto, conferências históricas exigem um indexador ou um segundo conjunto de dados construído de forma independente.
Saldos nativos em blockchains baseadas em contas
Para ETH e outras moedas nativas, o saldo no bloco N se decompõe em categorias armazenadas em conjuntos de dados distintos:
saldo(N) = valor recebido em transações de nível superior bem-sucedidas
+ valor recebido em chamadas internas bem-sucedidas (traces)
+ saques da camada de consenso e recompensas de destinatário de taxas
- valor enviado em transações de nível superior e chamadas internas bem-sucedidas
- taxas de gas pagas em cada transação enviada, bem-sucedida ou não
(Alocações do bloco gênese e recompensas de mineração anteriores ao Merge acrescentam parcelas para endereços mais antigos.) Exemplo prático para 0xA1b2…9F no bloco N:
| Item | Fonte | Valor (ETH) |
|---|---|---|
| Depósito de uma exchange | Transação | +5,000000 |
| Pagamento de um contrato | Trace (chamada interna) | +1,000000 |
| Saques do validador | Lista de saques do bloco | +0,032000 |
| Pagamento enviado | Transação | −2,500000 |
| Gas desse pagamento: 21.000 × 20 gwei | Recibo | −0,000420 |
| Transação com falha: valor não enviado | status do recibo = 0 | 0 |
| Gas da transação com falha: 70.000 × 30 gwei | Recibo | −0,002100 |
| Saldo derivado | 3,529480 |
Se eth_getBalance(0xA1b2…9F, N) retornar 3,529480 ETH, o histórico está completo. Os erros mais comuns deixam indícios na diferença. Ignorar traces subestima o saldo exatamente pelo crédito interno (1,0). Contar o valor da transação com falha subestima o saldo pelo valor da transação; esquecer o gas pago nessa transação o superestima em 0,0021. A ausência de saques produz uma diferença composta por valores pequenos e regulares. O gas é calculado como o gas usado multiplicado pelo preço efetivo do gas indicado no recibo, não pelo limite de gas, mais as taxas de blob para transações do tipo 3.
Saldos de tokens: eventos versus balanceOf
Os saldos de tokens ficam armazenados no contrato do token. Eles podem ser derivados pela soma dos eventos Transfer ou consultados com balanceOf(holder) por meio de eth_call no bloco N. Versão derivada em SQL, usando uma tabela de transferências decodificadas:
-- Saldos de tokens no bloco 21000000, derivados de eventos 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 -- token has 6 decimals
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Cada transferência gera duas linhas: um crédito para o destinatário e um débito para o remetente; a soma por titular corresponde ao saldo. As emissões aparecem como transferências a partir do endereço zero, então o endereço zero termina com um saldo negativo equivalente a tudo o que já foi emitido, menos tudo o que foi queimado para ele: uma conferência de oferta incorporada. Mantenha os valores inteiros brutos até a divisão final; converter cada linha para decimal antes disso acumula erros de arredondamento. Use um tipo decimal com grande capacidade, pois os valores brutos podem exceder os inteiros de 64 bits.
Eventos e balanceOf divergem por motivos conhecidos:
- Tokens com rebase alteram o saldo de todos os titulares por meio de um multiplicador global, sem um evento
Transferpor titular. As somas dos eventos ficam desatualizadas imediatamente. - Tokens que geram juros calculam
balanceOfcom base em um índice crescente; os eventos registram movimentações de principal, não os juros acumulados. - Tokens com taxa sobre transferência creditam ao destinatário menos do que o remetente envia; a maioria emite eventos com os valores líquidos, mas alguns emitem o valor bruto.
- Contratos não padronizados, em sua maioria mais antigos, emitem tokens sem gerar um evento
Transfer, ou as funções de congelamento e queima do emissor geram um evento personalizado.
Para esses tokens, o estado é a fonte da verdade, e os eventos contam apenas a história das transferências explícitas; a aula Como ler transferências de tokens aborda o tema em detalhes.
Capturas em uma altura de bloco
Um saldo “em 31 de março” é ambíguo até ser vinculado a um bloco. Converta o horário para o último bloco cujo timestamp seja igual ou anterior ao horário-limite, em UTC, e registre tanto a altura quanto o hash do bloco. No Ethereum, métodos de estado como eth_getBalance e eth_call aceitam um número de bloco ou, conforme a EIP-1898, um hash de bloco, o que garante que a resposta venha exatamente daquele bloco, mesmo que uma reorganização substitua posteriormente o bloco naquela altura. Consultar blocos antigos exige um nó que mantenha o estado histórico (um nó de arquivo ou um provedor).
Em capturas de várias blockchains, cada blockchain tem seu próprio bloco para o mesmo instante: os blocos do Bitcoin, com intervalos de aproximadamente 10 minutos, e os slots de 12 segundos do Ethereum oferecem granularidades diferentes; além disso, um bloco L2 de um rollup pode refletir depósitos que a L1 acabou de incluir. Informe no relatório o bloco escolhido para cada blockchain.
Conferências de reconciliação
A reconstrução não está concluída até ser conferida. Testes úteis:
- Derivado versus estado: compare os saldos derivados com o estado dos nós para cada endereço incluído no escopo ou, para conjuntos grandes, uma amostra aleatória em vários blocos.
- Conservação: a soma de todos os saldos derivados de tokens, excluindo o endereço zero, deve ser igual a
totalSupply()no mesmo bloco; em uma blockchain UTXO, o total do conjunto UTXO não deve exceder a emissão total. - Nenhum saldo negativo: um saldo acumulado negativo indica um crédito ausente, geralmente uma chamada interna, uma emissão sem evento ou um saque.
- Limites: o saldo antes da primeira atividade de um endereço é zero, e o saldo depois de uma varredura conhecida corresponde ao que restou após ela.
- Resíduos explicados: defina uma tolerância antecipadamente (por exemplo, zero para saldos nativos, um valor especificado para tokens com rebase) e documente a causa de cada diferença.
| Sintoma | Causa provável |
|---|---|
| Saldo derivado abaixo do esperado por valores que correspondem a pagamentos de contratos | Ausência de chamadas internas dos traces |
| Saldo derivado acima do esperado por valores pequenos | Taxas de gas de transações com falha não deduzidas |
| Créditos pequenos e regulares sem explicação | Saques da camada de consenso ou recompensas de destinatário de taxas |
Saldos dos titulares de tokens divergem diariamente de balanceOf | Token com rebase ou que gera juros |
A soma dos saldos difere de totalSupply() | Emissões ou queimas sem eventos Transfer |
Foco no exame: a diferença entre derivar um saldo do histórico e consultá-lo no estado, e por que os relatórios precisam de ambos; o cálculo de um saldo UTXO em uma altura e a conferência entre valores recebidos e enviados; os componentes de um saldo nativo de conta, especialmente chamadas internas, saques e gas em transações com falha; o cálculo do gas como gas usado × preço efetivo do gas; a derivação de saldos de tokens a partir de eventos Transfer e a conferência da oferta pelo endereço zero; por que tokens com rebase, que geram juros e com taxa sobre transferência invalidam as somas dos eventos; a fixação de capturas em uma altura e um hash de bloco (EIP-1898); conferências de reconciliação padrão e o que cada tipo de diferença sugere.