账户状态与stateTrie结构
以太坊通过stateTrie数据结构管理所有账户的状态信息。Block.Header.Root即stateRoot,是一棵PMT树,存储了所有账户的最新状态数据,包括账户余额等关键信息。
路径(path)的计算方式为:sha3(ethereumAddress),而值(value)的存储格式为:rlp(ethereumAccount)。Root作为哈希值,通过它可以找到stateTrie的根节点,再结合sha3(ethereumAddress)生成的路径,就能逐步定位到每个账户的rlp(ethereumAccount)数据。
账户结构解析
以太坊账户分为两种类型:普通账户余额和合约代币余额。账户的基本结构如下:
type Account struct {
Nonce uint64 // 账户发起交易的次数
Balance *big.Int // 该账户的余额
Root common.Hash // 存储树MPT,存储所有合约数据
CodeHash []byte // 合约代码的Hash值(仅对合约账户有效)
}每个用户对应一个StateObject,代表着stateTrie中的位置,反映了账户的动态变化结果:
type stateObject struct {
address common.Address
addrHash common.Hash // 以太坊地址的哈希值
data Account
db *StateDB
}余额查询实现原理
代码执行思路
查询余额的基本逻辑包含以下几个步骤:
- 获取当前区块高度,并从创世区块开始遍历所有区块(getBlockNumber)
- 获取特定区块信息,提取该区块中的所有交易TxHash并进行遍历(getBlock)
- 获取交易详细信息,识别转账地址from和接收地址to(getTransaction)
- 判断地址类型是合约地址还是普通账户地址(getCode)
- 获取地址对应的余额数据(getBalance)
查询流程详解
完整的余额查询流程包括:
- 查询获取当前最新区块,得到lastBlock.header.Root
- 先在本地缓存查找stateObject热点数据,若无则根据Root定位数据库中的根节点
- 按照Address在MPT树中的排列结构,找到对应的stateObject
- 通过stateObject获取对应的Account信息
- 从Account中提取余额数据
获取账户余额实战
代码解析与实现
读取账户余额非常简单。调用客户端的BalanceAt方法,传入账户地址和可选的区块号参数。将区块号设置为nil将返回最新余额。
account := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
balance, err := client.BalanceAt(context.Background(), account, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(balance) // 输出:25893180161173005034传入区块号可以查询历史余额,区块号必须为big.Int类型:
blockNumber := big.NewInt(5532993)
balance, err := client.BalanceAt(context.Background(), account, blockNumber)
if err != nil {
log.Fatal(err)
}
fmt.Println(balance) // 输出:25729324269165216042单位转换技巧
以太坊使用最小单位wei处理数字,因为它们是定点精度。要将wei转换为ETH,需要除以10^18。由于处理的是大数,需要导入math和math/big包:
fbalance := new(big.Float)
fbalance.SetString(balance.String())
ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18)))
fmt.Println(ethValue) // 输出:25.729324269165216041待处理余额查询
有时需要查询待确认的账户余额,例如在提交交易后等待确认时。客户端提供了PendingBalanceAt方法,用法与BalanceAt类似:
pendingBalance, err := client.PendingBalanceAt(context.Background(), account)
fmt.Println(pendingBalance) // 输出:25729324269165216042完整代码示例
package main
import (
"context"
"fmt"
"log"
"math"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("https://mainnet.infura.io")
if err != nil {
log.Fatal(err)
}
account := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
balance, err := client.BalanceAt(context.Background(), account, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(balance) // 25893180161173005034
blockNumber := big.NewInt(5532993)
balanceAt, err := client.BalanceAt(context.Background(), account, blockNumber)
if err != nil {
log.Fatal(err)
}
fmt.Println(balanceAt) // 25729324269165216042
fbalance := new(big.Float)
fbalance.SetString(balanceAt.String())
ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18)))
fmt.Println(ethValue) // 25.729324269165216041
pendingBalance, err := client.PendingBalanceAt(context.Background(), account)
fmt.Println(pendingBalance) // 25729324269165216042
}获取账户代币余额
参数获取方法
获取代币余额需要from、to和data参数:
func (rc *RequestChain) GetCallContractInfo(from, to, data string) (string, error) {
info, err := rc.Client.CallContract(from, to, data)
if err != nil {
logger.Error("GetCallContractInfo", "step", "CallContract", "err", err.Error())
return "", err
}
res, err := common.HexToString(info.(string))
if err != nil {
logger.Error("GetCallContractInfo", "step", "HexToString", "err", err.Error())
return "", err
}
resByte, err := hex.DecodeString(res)
return string(resByte), nil
}以太坊RPC调用
通过调用以太坊RPC接口查询合约余额:
// CallContract 查询合约
func (eth *Http) CallContract(from, to, data string) (interface{}, error) {
tag := "latest"
args = []interface{}{CallMsg{
From: from,
To: to,
Data: data,
}, tag}
params := NewHttpParams("eth_call", args)
resBody, err := eth.rpc.HttpRequest(params)
if err != nil {
return nil, err
}
return eth.ParseJsonRPCResponse(resBody)
}常见问题
什么是以太坊stateTrie?
stateTrie是以太坊中存储所有账户状态的数据结构,采用PMT树形式组织。它包含了每个账户的余额、nonce、合约存储根和代码哈希等信息,通过区块头中的stateRoot可以访问到特定区块时的全局状态。
如何查询历史区块的账户余额?
使用BalanceAt方法时,除了传入账户地址外,还需要指定要查询的区块号。区块号需要转换为big.Int类型,这样可以获取到该区块确认时的账户余额状态。
待处理余额与确认余额有何区别?
待处理余额(PendingBalanceAt)反映的是包含尚未被区块确认的交易后的余额,而确认余额(BalanceAt)只计算已经被区块确认的交易。在交易等待期间,查询待处理余额可以预估交易完成后的账户状态。
如何正确转换wei和ETH单位?
以太坊使用wei作为最小单位,1 ETH = 10^18 wei。转换时需要使