前言

随着区块链技术的不断发展,以太坊作为去中心化应用的领先平台,在开发者社区中占据了重要地位。Geth (Go-Ethereum) 是以太坊官方提供的开源客户端,允许开发者通过命令行、API 等方式与以太坊网络进行交互。然而,在某些特定场景下,直接通过 Geth 的接口获取数据可能会受到性能和灵活性的限制。因此,开发者可能希望直接访问 Geth 的本地数据库,以更高效地读取链上数据,如账户余额、区块头信息等。

本文将介绍如何使用 Go 语言打开 Geth 的本地数据库,并展示如何通过直接读取数据库来获取账户余额等信息。这种方法不仅能够提升数据访问的灵活性,还为区块链开发者提供了更深入理解以太坊底层数据结构的机会。
在这里插入图片描述


Geth (Go-Ethereum) 使用 LevelDB 存储链上数据

Geth (Go-Ethereum) 是以太坊的官方客户端之一,它通过区块链数据的存储和查询来支持以太坊节点的运行。在 Geth 中,数据的本地存储采用了 LevelDB,这是一个高性能的键值存储数据库,能够高效处理大量的链上数据。

应用场景

  1. 账户状态(Account State): 每个以太坊账户的状态,包括余额、nonce、存储、合约代码等,都会被存储在 LevelDB 中。这些账户状态信息会根据用户的操作(如转账、智能合约执行等)动态更新。

  2. 交易信息(Transaction Data): 每笔以太坊交易的数据也会存储在 LevelDB 中。交易数据包括发送方和接收方的地址、交易金额、手续费、交易签名等详细信息。

  3. 区块数据(Block Data): Geth 使用 LevelDB 来存储完整的区块数据。每个区块包含区块头(block header)、区块体(block body,包括交易列表和状态树)、矿工信息、时间戳等。LevelDB 通过键值对的方式高效存储这些数据,并支持快速检索。

  4. 交易索引(Transaction Index): 为了加速交易查询,Geth 将每笔交易与区块号和区块内的交易索引关联起来。这些索引信息也保存在 LevelDB 中,用于支持通过交易哈希快速定位交易的功能。

  5. 区块索引(Block Index): 区块链的区块是按顺序链接的,每个区块都记录了前一个区块的哈希。Geth 使用 LevelDB 存储区块索引,支持通过区块哈希快速定位到具体的区块数据。

为什么 Geth 选择 LevelDB?

  • 高效的读写性能LevelDB 采用 LSM 树结构,在写入时非常高效,并且支持快速的顺序和随机读取,特别适合区块链这种需要频繁更新和查询的场景。

  • 嵌入式数据库LevelDB 是一个嵌入式数据库,无需额外的服务进程,这使得它更适合像 Geth 这样独立运行的节点程序。

  • 空间优化LevelDB 支持数据压缩,能够有效减少区块链庞大数据的存储空间需求。

  • 键值存储灵活性:以太坊中的数据可以通过哈希键快速定位,LevelDB 的键值对存储结构非常适合这种需求。

通过 LevelDBGeth 实现了对以太坊链上海量数据的高效存储与读取,为以太坊节点的运行提供了可靠的数据库支持。这种存储方式能够保证以太坊客户端在处理数十亿条交易和区块时依然保持良好的性能


环境准备

在开始之前,我们需要准备以下环境和库:

  • Go 语言环境:确保你的开发环境已安装 Go 语言。
    在这里插入图片描述

  • Geth 客户端:Geth 客户端已经同步了区块链,并且包含了 chaindata 文件夹(1.13.14版本geth安装教程🚪
    在这里插入图片描述

  • Pebble:一个键值存储库,Geth 使用它来存储本地区块链数据。本文的示例代码将使用 github.com/cockroachdb/pebble 来处理数据库读取操作。

  • 其他依赖库
    github.com/ethereum/go-ethereum:用于以太坊的常用工具和数据结构。
    golang.org/x/crypto/sha3:用于计算 Keccak-256 哈希值。


代码说明

1. 打开 Pebble 数据库

代码中的 pebble.Open 函数用于打开 Geth 的本地数据库,该数据库通常存储在 Geth 的 chaindata 文件夹中。我们通过指定数据库路径 dbPath 来打开它,并使用 defer 来确保程序结束时正确关闭数据库。

db, err := pebble.Open(dbPath, &pebble.Options{})
if err != nil {
    log.Fatalf("无法打开数据库: %v", err)
}
defer db.Close()

2. 读取账户余额

函数 readAccountBalance 接受数据库连接和账户地址,通过计算该地址的 Keccak-256 哈希值来构造查询键。然后,它从数据库中检索账户的原始数据并解码出账户的余额。

func readAccountBalance(db *pebble.DB, address string) {
    addrBytes := common.HexToAddress(address).Bytes()
    key := append([]byte("a"), crypto.Keccak256(addrBytes)...)
    value, closer, err := db.Get(key)
    if err != nil {
        if err == pebble.ErrNotFound {
            fmt.Println("未找到账户数据,可能是新账户或余额为0")
        } else {
            log.Printf("读取账户数据时发生错误: %v\n", err)
        }
        return
    }
    defer closer.Close()
    // 解码账户数据,获取余额
    var accountData []interface{}
    if err := rlp.DecodeBytes(value, &accountData); err != nil {
        log.Printf("无法解码 RLP 数据: %v\n", err)
        return
    }
    balanceBytes, ok := accountData[1].([]byte)
    if ok {
        balance := new(big.Int).SetBytes(balanceBytes)
        fmt.Printf("账户余额: %s wei\n", balance.String())
    }
}

3. 统计账户数量

通过迭代数据库中的键值对,countAccounts 函数可以遍历并统计所有以字母 ‘a’ 开头的账户数据。

func countAccounts(db *pebble.DB) {
    iter, err := db.NewIter(&pebble.IterOptions{})
    if err != nil {
        log.Fatalf("创建迭代器失败: %v", err)
    }
    defer iter.Close()

    accountCount := 0
    for iter.First(); iter.Valid(); iter.Next() {
        key := iter.Key()
        if len(key) > 0 && key[0] == 'a' { // 判断是否是账户数据
            accountCount++
        }
    }
    fmt.Printf("总共发现 %d 个账户\n", accountCount)
}

4. 读取区块头信息

readBasicInfo 函数展示了如何读取最新的区块头哈希和区块号。首先,它读取存储在 LastBlock 键下的最新区块头哈希值,然后使用该哈希值读取相应的区块头数据。

func readBasicInfo(db *pebble.DB) {
    headBlockHashKey := []byte("LastBlock")
    value, closer, err := db.Get(headBlockHashKey)
    headBlockHash := common.BytesToHash(value)
    headerNumberKey := append([]byte("H"), headBlockHash.Bytes()...)
    numberBytes, closer, err := db.Get(headerNumberKey)
    blockNumber := binary.BigEndian.Uint64(numberBytes)
    fmt.Printf("最新区块号: %d\n", blockNumber)
}

5. 读取原始数据库数据

readRawData 函数使用迭代器从数据库中读取一些原始数据。该函数每次最多读取 10 个键值对,并打印其十六进制表示形式。

func readRawData(db *pebble.DB) {
    iter, err := db.NewIter(&pebble.IterOptions{})
    defer iter.Close()
    count := 0
    for iter.First(); iter.Valid() && count < 10; iter.Next() {
        fmt.Printf("Key: %s, Value: %s\n", hex.EncodeToString(iter.Key()), hex.EncodeToString(iter.Value()))
        count++
    }
}

运行实例

以下是一个完整的读取geth数据的 Go 代码示例,它展示了如何打开 Geth 的本地数据库,读取账户余额,并打印一些账户和区块头信息:
在这里插入图片描述

更换配置信息

在下面的代码的39行替换为自己geth私链的chaindata文件夹路径
在这里插入图片描述

替换为自己的地址
在这里插入图片描述

package main

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"log"
	"math/big"

	"github.com/cockroachdb/pebble"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/rlp"
	"golang.org/x/crypto/sha3"
)

// 判断一个键是否是交易索引键
func isTransactionIndexKey(key []byte) bool {
	// 这个判断逻辑需要根据实际的键结构进行调整
	// 这里我们简单假设长度和某些特定字节序列来判断
	return len(key) > 40
}

func getKeccak256Key(address string) string {
	// 去掉地址中的 "0x" 前缀
	addrBytes := common.HexToAddress(address).Bytes()

	// 计算 Keccak-256 哈希
	hash := sha3.NewLegacyKeccak256()
	hash.Write(addrBytes)
	hashed := hash.Sum(nil)

	// 返回哈希值的十六进制字符串表示
	return hex.EncodeToString(hashed)
}

func main() {
	dbPath := "D:/hjy/geth1.13.14/geth/chaindata"
	db, err := pebble.Open(dbPath, &pebble.Options{})
	if err != nil {
		log.Fatalf("无法打开数据库: %v", err)
	}
	defer db.Close()

	fmt.Println("成功打开数据库")

	readAccountBalance(db, "0x6643714dc8a254b4a44ce24d2d23715b670aa141")
	readAccountBalance(db, "0xa2ce3B611e5B13DF80793Cccd0772c122e990A95")

	countAccounts(db)
	address := "0x6643714dc8a254b4a44ce24d2d23715b670aa141"
	key := getKeccak256Key(address)
	fmt.Printf("地址 %s 的 Keccak-256 哈希为: %s\n", address, key)
	readRawData(db)

}

func countAccounts(db *pebble.DB) {
	fmt.Println("\n统计账户数量:")

	iter, err := db.NewIter(&pebble.IterOptions{})
	if err != nil {
		log.Fatalf("创建迭代器失败: %v", err)
	}
	defer iter.Close()

	accountCount := 0

	for iter.First(); iter.Valid(); iter.Next() {
		key := iter.Key()

		if len(key) > 0 && key[0] == 'a' { // 判断是否是账户数据

			value := iter.Value()
			fmt.Printf("账户key:%x  value:%x\n", key, value)
			var accountData []interface{}
			if err := rlp.DecodeBytes(value, &accountData); err == nil {
				accountCount++
			}
		}
	}

	if err := iter.Error(); err != nil {
		log.Printf("迭代错误: %v", err)
	}

	fmt.Printf("总共发现 %d 个账户\n", accountCount)
}

func readAccountBalance(db *pebble.DB, address string) {
	fmt.Printf("\n尝试读取账户 %s 的余额:\n", address)

	addrBytes := common.HexToAddress(address).Bytes()
	key := append([]byte("a"), crypto.Keccak256(addrBytes)...)

	fmt.Printf("查询键: %x\n", key)

	value, closer, err := db.Get(key)
	if err != nil {
		if err == pebble.ErrNotFound {
			fmt.Printf("未找到账户数据,可能是新账户或余额为0\n")
		} else {
			log.Printf("读取账户数据时发生错误: %v\n", err)
		}
		return
	}
	defer closer.Close()

	fmt.Printf("原始数据: %x\n", value)

	// 手动解码 RLP 数据
	var accountData []interface{}
	if err := rlp.DecodeBytes(value, &accountData); err != nil {
		log.Printf("无法解码 RLP 数据: %v\n", err)
		return
	}

	fmt.Println("解码后的账户数据:")
	for i, field := range accountData {
		fmt.Printf("字段 %d: %v\n", i, field)
	}

	// 尝试提取余额
	if len(accountData) > 1 {
		balanceBytes, ok := accountData[1].([]byte)
		if ok {
			balance := new(big.Int).SetBytes(balanceBytes)
			fmt.Printf("账户余额: %s wei\n", balance.String())
		} else {
			fmt.Println("无法提取余额字段")
		}
	} else {
		fmt.Println("账户数据格式不符合预期")
	}
}
func readBasicInfo(db *pebble.DB) {
	// 读取最新区块头哈希
	headBlockHashKey := []byte("LastBlock")
	value, closer, err := db.Get(headBlockHashKey)
	if err != nil {
		log.Printf("无法读取最新区块头哈希: %v", err)
		return
	}
	defer closer.Close()
	headBlockHash := common.BytesToHash(value)
	fmt.Printf("最新区块头哈希: %x\n", headBlockHash)

	// 读取最新区块号
	headerNumberKey := append([]byte("H"), headBlockHash.Bytes()...)
	numberBytes, closer, err := db.Get(headerNumberKey)
	if err != nil {
		log.Printf("无法读取最新区块号: %v", err)
		return
	}
	defer closer.Close()
	blockNumber := binary.BigEndian.Uint64(numberBytes)
	fmt.Printf("最新区块号: %d\n", blockNumber)

	// 读取区块头
	headerKey := append([]byte("h"), append(numberBytes, headBlockHash.Bytes()...)...)
	headerData, closer, err := db.Get(headerKey)
	if err != nil {
		log.Printf("无法读取区块头: %v", err)
		log.Printf("尝试的键: %x", headerKey)
		return
	}
	defer closer.Close()
	var header types.Header
	if err := rlp.DecodeBytes(headerData, &header); err != nil {
		log.Printf("无法解码区块头: %v", err)
		return
	}
	fmt.Printf("最新区块时间戳: %v\n", header.Time)

	// 读取创世区块哈希
	genesisNumberBytes := make([]byte, 8)
	genesisHashKey := append([]byte("h"), genesisNumberBytes...)
	genesisValue, closer, err := db.Get(genesisHashKey)
	if err != nil {
		log.Printf("无法读取创世区块哈希: %v", err)
		return
	}
	defer closer.Close()
	genesisHash := common.BytesToHash(genesisValue)
	fmt.Printf("创世区块哈希: %x\n", genesisHash)
}

func readRawData(db *pebble.DB) {
	fmt.Println("\n尝试直接读取一些原始数据:")
	iter, err := db.NewIter(&pebble.IterOptions{})
	if err != nil {
		log.Fatalf("创建迭代器失败: %v", err)
	}
	defer iter.Close()

	count := 0
	for iter.First(); iter.Valid() && count < 10; iter.Next() {
		fmt.Printf("Key: %s, Value: %s\n", hex.EncodeToString(iter.Key()), hex.EncodeToString(iter.Value()))
		count++
	}

	if err := iter.Error(); err != nil {
		log.Printf("迭代错误: %v", err)
	}

	if count == 0 {
		fmt.Println("数据库似乎是空的")
	}
}

下载依赖

将以上代码放入项目后,用以下命令下载依赖:

  1. 生成go mod
go mod init readGETH

在这里插入图片描述

  1. 更新go mod
    这条命令将会下载所需的所有依赖包,如pebble库和以太坊的go-ethereum
go mod tidy

在这里插入图片描述

启动项目

go run readpebble.go

可以看到没有启动geth私链也查到了链上的数据
在这里插入图片描述

这时再启动geth并打开geth控制台验证一下是否是正确,可以看到账户和金额都对上了!!!

打开geth控制台的方法,第二个是自己geth启动的路径:
geth attach \\.\pipe\geth.ipc
geth attach http://127.0.0.1:8888

在这里插入图片描述


总结

本文介绍了如何使用 Go 语言打开 Geth 的本地数据库,并通过读取 Pebble 数据库来获取以太坊账户的余额信息。通过这种方式,开发者可以绕过 Geth 的接口,直接从数据库中高效读取链上数据。这不仅提升了数据查询的灵活性,还帮助开发者更深入地理解了 Geth 底层的数据存储结构。

你可以将该方法应用到更多复杂的场景中,如批量查询账户数据、分析区块链交易历史等。通过直接操作 Geth 的本地数据库,开发者将获得更加丰富的链上数据分析能力。如果你有任何疑问或建议,欢迎在评论区留言讨论🌹

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐