mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 14:46:42 +00:00
cmd/geth: add dump-balances command to export addresses with non-zero ETH
This commit is contained in:
parent
51b34efebc
commit
16a0421511
2 changed files with 173 additions and 0 deletions
172
cmd/geth/dumpbalancescmd.go
Normal file
172
cmd/geth/dumpbalancescmd.go
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dumpBalancesCommand = &cli.Command{
|
||||||
|
Name: "dump-balances",
|
||||||
|
Usage: "Export all non-zero accounts from current state to a file",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
utils.DataDirFlag,
|
||||||
|
utils.NetworkIdFlag,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "out",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Output file path",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose logging",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: dumpBalances,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
app.Commands = append(app.Commands, dumpBalancesCommand)
|
||||||
|
sort.Sort(cli.CommandsByName(app.Commands))
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpBalances(ctx *cli.Context) error {
|
||||||
|
// Connect to Ethereum node
|
||||||
|
stack, service, err := connectEthereum(
|
||||||
|
ctx.String(utils.DataDirFlag.Name),
|
||||||
|
ctx.Uint64(utils.NetworkIdFlag.Name),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := stack.Close(); cerr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: failed to close node: %v\n", cerr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get state database at latest header
|
||||||
|
stateDB, err := getStateDB(service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch non-zero balances
|
||||||
|
entries, err := fetchBalances(stateDB, ctx.Bool("verbose"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine output path
|
||||||
|
outPath := ctx.String("out")
|
||||||
|
if outPath == "" {
|
||||||
|
outPath = filepath.Join(ctx.String(utils.DataDirFlag.Name), "addresses_balances.txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write results
|
||||||
|
if err := writeBalances(outPath, entries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Dump completed: %s\n", outPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectEthereum initializes and starts an Ethereum node and service
|
||||||
|
func connectEthereum(dataDir string, networkID uint64) (*node.Node, *eth.Ethereum, error) {
|
||||||
|
cfg := &node.Config{DataDir: dataDir}
|
||||||
|
stack, err := node.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create node: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ethCfg := ð.Config{NetworkId: networkID}
|
||||||
|
service, err := eth.New(stack, ethCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create eth service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.Start(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to start node: %w", err)
|
||||||
|
}
|
||||||
|
return stack, service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStateDB retrieves the state database at the current head
|
||||||
|
func getStateDB(service *eth.Ethereum) (*state.StateDB, error) {
|
||||||
|
header := service.BlockChain().CurrentHeader()
|
||||||
|
stateDB, err := service.BlockChain().StateAt(header.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get state at root: %w", err)
|
||||||
|
}
|
||||||
|
return stateDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type accountEntry struct {
|
||||||
|
Address common.Address
|
||||||
|
Balance *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchBalances scans all accounts and returns those with non-zero balance
|
||||||
|
func fetchBalances(stateDB *state.StateDB, verbose bool) ([]accountEntry, error) {
|
||||||
|
dump := stateDB.RawDump(&state.DumpConfig{SkipCode: true, SkipStorage: true})
|
||||||
|
|
||||||
|
var entries []accountEntry
|
||||||
|
processedCount := 0
|
||||||
|
for _, acc := range dump.Accounts {
|
||||||
|
processedCount++
|
||||||
|
if acc.Address == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
balInt, ok := new(big.Int).SetString(acc.Balance, 10)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid balance for account %s: %s", acc.Address.Hex(), acc.Balance)
|
||||||
|
}
|
||||||
|
if balInt.Sign() > 0 {
|
||||||
|
entries = append(entries, accountEntry{*acc.Address, balInt})
|
||||||
|
}
|
||||||
|
if verbose && processedCount%100000 == 0 {
|
||||||
|
fmt.Printf("Processed %d accounts, %d non-zero so far\n", processedCount, len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort descending by balance
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Balance.Cmp(entries[j].Balance) > 0
|
||||||
|
})
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBalances writes the sorted balances to the specified file
|
||||||
|
func writeBalances(path string, entries []accountEntry) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := f.Close(); cerr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: failed to close file: %v\n", cerr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Prepare divisor for Wei to Ether conversion
|
||||||
|
denom := new(big.Float).SetInt(big.NewInt(1_000_000_000_000_000_000))
|
||||||
|
buf := new(big.Float)
|
||||||
|
for _, e := range entries {
|
||||||
|
buf.Quo(new(big.Float).SetInt(e.Balance), denom)
|
||||||
|
if _, err := fmt.Fprintf(f, "%s\t%.6f\n", e.Address.Hex(), buf); err != nil {
|
||||||
|
return fmt.Errorf("failed to write to file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -224,6 +224,7 @@ func init() {
|
||||||
importPreimagesCommand,
|
importPreimagesCommand,
|
||||||
removedbCommand,
|
removedbCommand,
|
||||||
dumpCommand,
|
dumpCommand,
|
||||||
|
dumpBalancesCommand,
|
||||||
dumpGenesisCommand,
|
dumpGenesisCommand,
|
||||||
pruneCommand,
|
pruneCommand,
|
||||||
// See accountcmd.go:
|
// See accountcmd.go:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue