c9e0b3105b
Introduces the first built-in live tracer. The supply tracer tracks ETH supply changes across blocks and writes the output to disk. This will need to be enabled through CLI using the `--vmtrace supply` flag. Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
311 lines
8.4 KiB
Go
311 lines
8.4 KiB
Go
package live
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"path/filepath"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
|
|
"github.com/ethereum/go-ethereum/core/tracing"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/eth/tracers"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
func init() {
|
|
tracers.LiveDirectory.Register("supply", newSupply)
|
|
}
|
|
|
|
type supplyInfoIssuance struct {
|
|
GenesisAlloc *big.Int `json:"genesisAlloc,omitempty"`
|
|
Reward *big.Int `json:"reward,omitempty"`
|
|
Withdrawals *big.Int `json:"withdrawals,omitempty"`
|
|
}
|
|
|
|
//go:generate go run github.com/fjl/gencodec -type supplyInfoIssuance -field-override supplyInfoIssuanceMarshaling -out gen_supplyinfoissuance.go
|
|
type supplyInfoIssuanceMarshaling struct {
|
|
GenesisAlloc *hexutil.Big
|
|
Reward *hexutil.Big
|
|
Withdrawals *hexutil.Big
|
|
}
|
|
|
|
type supplyInfoBurn struct {
|
|
EIP1559 *big.Int `json:"1559,omitempty"`
|
|
Blob *big.Int `json:"blob,omitempty"`
|
|
Misc *big.Int `json:"misc,omitempty"`
|
|
}
|
|
|
|
//go:generate go run github.com/fjl/gencodec -type supplyInfoBurn -field-override supplyInfoBurnMarshaling -out gen_supplyinfoburn.go
|
|
type supplyInfoBurnMarshaling struct {
|
|
EIP1559 *hexutil.Big
|
|
Blob *hexutil.Big
|
|
Misc *hexutil.Big
|
|
}
|
|
|
|
type supplyInfo struct {
|
|
Issuance *supplyInfoIssuance `json:"issuance,omitempty"`
|
|
Burn *supplyInfoBurn `json:"burn,omitempty"`
|
|
|
|
// Block info
|
|
Number uint64 `json:"blockNumber"`
|
|
Hash common.Hash `json:"hash"`
|
|
ParentHash common.Hash `json:"parentHash"`
|
|
}
|
|
|
|
type supplyTxCallstack struct {
|
|
calls []supplyTxCallstack
|
|
burn *big.Int
|
|
}
|
|
|
|
type supply struct {
|
|
delta supplyInfo
|
|
txCallstack []supplyTxCallstack // Callstack for current transaction
|
|
logger *lumberjack.Logger
|
|
}
|
|
|
|
type supplyTracerConfig struct {
|
|
Path string `json:"path"` // Path to the directory where the tracer logs will be stored
|
|
MaxSize int `json:"maxSize"` // MaxSize is the maximum size in megabytes of the tracer log file before it gets rotated. It defaults to 100 megabytes.
|
|
}
|
|
|
|
func newSupply(cfg json.RawMessage) (*tracing.Hooks, error) {
|
|
var config supplyTracerConfig
|
|
if cfg != nil {
|
|
if err := json.Unmarshal(cfg, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config: %v", err)
|
|
}
|
|
}
|
|
if config.Path == "" {
|
|
return nil, errors.New("supply tracer output path is required")
|
|
}
|
|
|
|
// Store traces in a rotating file
|
|
logger := &lumberjack.Logger{
|
|
Filename: filepath.Join(config.Path, "supply.jsonl"),
|
|
}
|
|
if config.MaxSize > 0 {
|
|
logger.MaxSize = config.MaxSize
|
|
}
|
|
|
|
t := &supply{
|
|
delta: newSupplyInfo(),
|
|
logger: logger,
|
|
}
|
|
return &tracing.Hooks{
|
|
OnBlockStart: t.OnBlockStart,
|
|
OnBlockEnd: t.OnBlockEnd,
|
|
OnGenesisBlock: t.OnGenesisBlock,
|
|
OnTxStart: t.OnTxStart,
|
|
OnBalanceChange: t.OnBalanceChange,
|
|
OnEnter: t.OnEnter,
|
|
OnExit: t.OnExit,
|
|
OnClose: t.OnClose,
|
|
}, nil
|
|
}
|
|
|
|
func newSupplyInfo() supplyInfo {
|
|
return supplyInfo{
|
|
Issuance: &supplyInfoIssuance{
|
|
GenesisAlloc: big.NewInt(0),
|
|
Reward: big.NewInt(0),
|
|
Withdrawals: big.NewInt(0),
|
|
},
|
|
Burn: &supplyInfoBurn{
|
|
EIP1559: big.NewInt(0),
|
|
Blob: big.NewInt(0),
|
|
Misc: big.NewInt(0),
|
|
},
|
|
|
|
Number: 0,
|
|
Hash: common.Hash{},
|
|
ParentHash: common.Hash{},
|
|
}
|
|
}
|
|
|
|
func (s *supply) resetDelta() {
|
|
s.delta = newSupplyInfo()
|
|
}
|
|
|
|
func (s *supply) OnBlockStart(ev tracing.BlockEvent) {
|
|
s.resetDelta()
|
|
|
|
s.delta.Number = ev.Block.NumberU64()
|
|
s.delta.Hash = ev.Block.Hash()
|
|
s.delta.ParentHash = ev.Block.ParentHash()
|
|
|
|
// Calculate Burn for this block
|
|
if ev.Block.BaseFee() != nil {
|
|
burn := new(big.Int).Mul(new(big.Int).SetUint64(ev.Block.GasUsed()), ev.Block.BaseFee())
|
|
s.delta.Burn.EIP1559 = burn
|
|
}
|
|
// Blob burnt gas
|
|
if blobGas := ev.Block.BlobGasUsed(); blobGas != nil && *blobGas > 0 && ev.Block.ExcessBlobGas() != nil {
|
|
var (
|
|
excess = *ev.Block.ExcessBlobGas()
|
|
baseFee = eip4844.CalcBlobFee(excess)
|
|
burn = new(big.Int).Mul(new(big.Int).SetUint64(*blobGas), baseFee)
|
|
)
|
|
s.delta.Burn.Blob = burn
|
|
}
|
|
}
|
|
|
|
func (s *supply) OnBlockEnd(err error) {
|
|
s.write(s.delta)
|
|
}
|
|
|
|
func (s *supply) OnGenesisBlock(b *types.Block, alloc types.GenesisAlloc) {
|
|
s.resetDelta()
|
|
|
|
s.delta.Number = b.NumberU64()
|
|
s.delta.Hash = b.Hash()
|
|
s.delta.ParentHash = b.ParentHash()
|
|
|
|
// Initialize supply with total allocation in genesis block
|
|
for _, account := range alloc {
|
|
s.delta.Issuance.GenesisAlloc.Add(s.delta.Issuance.GenesisAlloc, account.Balance)
|
|
}
|
|
|
|
s.write(s.delta)
|
|
}
|
|
|
|
func (s *supply) OnBalanceChange(a common.Address, prevBalance, newBalance *big.Int, reason tracing.BalanceChangeReason) {
|
|
diff := new(big.Int).Sub(newBalance, prevBalance)
|
|
|
|
// NOTE: don't handle "BalanceIncreaseGenesisBalance" because it is handled in OnGenesisBlock
|
|
switch reason {
|
|
case tracing.BalanceIncreaseRewardMineUncle:
|
|
case tracing.BalanceIncreaseRewardMineBlock:
|
|
s.delta.Issuance.Reward.Add(s.delta.Issuance.Reward, diff)
|
|
case tracing.BalanceIncreaseWithdrawal:
|
|
s.delta.Issuance.Withdrawals.Add(s.delta.Issuance.Withdrawals, diff)
|
|
case tracing.BalanceDecreaseSelfdestructBurn:
|
|
// BalanceDecreaseSelfdestructBurn is non-reversible as it happens
|
|
// at the end of the transaction.
|
|
s.delta.Burn.Misc.Sub(s.delta.Burn.Misc, diff)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *supply) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) {
|
|
s.txCallstack = make([]supplyTxCallstack, 0, 1)
|
|
}
|
|
|
|
// internalTxsHandler handles internal transactions burned amount
|
|
func (s *supply) internalTxsHandler(call *supplyTxCallstack) {
|
|
// Handle Burned amount
|
|
if call.burn != nil {
|
|
s.delta.Burn.Misc.Add(s.delta.Burn.Misc, call.burn)
|
|
}
|
|
|
|
if len(call.calls) > 0 {
|
|
// Recursivelly handle internal calls
|
|
for _, call := range call.calls {
|
|
callCopy := call
|
|
s.internalTxsHandler(&callCopy)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *supply) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
|
call := supplyTxCallstack{
|
|
calls: make([]supplyTxCallstack, 0),
|
|
}
|
|
|
|
// This is a special case of burned amount which has to be handled here
|
|
// which happens when type == selfdestruct and from == to.
|
|
if vm.OpCode(typ) == vm.SELFDESTRUCT && from == to && value.Cmp(common.Big0) == 1 {
|
|
call.burn = value
|
|
}
|
|
|
|
// Append call to the callstack, so we can fill the details in CaptureExit
|
|
s.txCallstack = append(s.txCallstack, call)
|
|
}
|
|
|
|
func (s *supply) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
|
|
if depth == 0 {
|
|
// No need to handle Burned amount if transaction is reverted
|
|
if !reverted {
|
|
s.internalTxsHandler(&s.txCallstack[0])
|
|
}
|
|
return
|
|
}
|
|
|
|
size := len(s.txCallstack)
|
|
if size <= 1 {
|
|
return
|
|
}
|
|
// Pop call
|
|
call := s.txCallstack[size-1]
|
|
s.txCallstack = s.txCallstack[:size-1]
|
|
size -= 1
|
|
|
|
// In case of a revert, we can drop the call and all its subcalls.
|
|
// Caution, that this has to happen after popping the call from the stack.
|
|
if reverted {
|
|
return
|
|
}
|
|
s.txCallstack[size-1].calls = append(s.txCallstack[size-1].calls, call)
|
|
}
|
|
|
|
func (s *supply) OnClose() {
|
|
if err := s.logger.Close(); err != nil {
|
|
log.Warn("failed to close supply tracer log file", "error", err)
|
|
}
|
|
}
|
|
|
|
func (s *supply) write(data any) {
|
|
supply, ok := data.(supplyInfo)
|
|
if !ok {
|
|
log.Warn("failed to cast supply tracer data on write to log file")
|
|
return
|
|
}
|
|
|
|
// Remove empty fields
|
|
if supply.Issuance.GenesisAlloc.Sign() == 0 {
|
|
supply.Issuance.GenesisAlloc = nil
|
|
}
|
|
|
|
if supply.Issuance.Reward.Sign() == 0 {
|
|
supply.Issuance.Reward = nil
|
|
}
|
|
|
|
if supply.Issuance.Withdrawals.Sign() == 0 {
|
|
supply.Issuance.Withdrawals = nil
|
|
}
|
|
|
|
if supply.Issuance.GenesisAlloc == nil && supply.Issuance.Reward == nil && supply.Issuance.Withdrawals == nil {
|
|
supply.Issuance = nil
|
|
}
|
|
|
|
if supply.Burn.EIP1559.Sign() == 0 {
|
|
supply.Burn.EIP1559 = nil
|
|
}
|
|
|
|
if supply.Burn.Blob.Sign() == 0 {
|
|
supply.Burn.Blob = nil
|
|
}
|
|
|
|
if supply.Burn.Misc.Sign() == 0 {
|
|
supply.Burn.Misc = nil
|
|
}
|
|
|
|
if supply.Burn.EIP1559 == nil && supply.Burn.Blob == nil && supply.Burn.Misc == nil {
|
|
supply.Burn = nil
|
|
}
|
|
|
|
out, _ := json.Marshal(supply)
|
|
if _, err := s.logger.Write(out); err != nil {
|
|
log.Warn("failed to write to supply tracer log file", "error", err)
|
|
}
|
|
if _, err := s.logger.Write([]byte{'\n'}); err != nil {
|
|
log.Warn("failed to write to supply tracer log file", "error", err)
|
|
}
|
|
}
|