写入:
以太坊区块生成并写入区块链数据库,分为创世区块和普通区块两种。其写入过程是相同的,区别在于区块生成过程。以生成创世区块为例子,总体流程是从genesis.json读取配置,写入内存的数据结构,再写入磁盘leveldb文件。
(注:以上是go语言版本geth的分析, java版本Ethereumj目前没有命令行init的功能,需要自行调用相关类去实现)
在core/genesis.go中WriteGenesisBlock函数将创世区块写入数据库作为第0号区块,其函数入参是数据库链接实例和ioReader实例,输出一个块Block对象。
函数定义:
func WriteGenesisBlock(chainDb ethdb.Database, reader io.Reader)
创世区块生成时,按以太坊黄皮书要求,需要配置文件genesis.json将一些参数传给区块链应用
contents, err := ioutil.ReadAll(reader) //读取了磁盘上的genesis.json
读取配置文件后,如下是内存中对应的结构:
var genesis struct {
ChainConfig *params.ChainConfig `json:"config"`
Nonce string //工作量证明的随机数,用于挖矿算法
Timestamp string //创建时机器时间,自19700101起到区块产生时的总秒数
ParentHash string //创世区块没有父块,一般设置为全0
ExtraData string //创造者留言,比如比特币的这个写的是泰晤士报纸的标题
GasLimit string //区块所允许交易消耗gas的最大量
Difficulty string //挖矿难度,数值越大代表困难越高
Mixhash string //挖矿过程所产生的中间过程hash值
Coinbase string //矿工的主账号,挖矿收益存入本账号
Alloc map[string]struct { //预置账号
Code string
Storage map[string]string
Balance string
}
}
读取创世区块配置文件后,对数据库需要如下2个动作,一是要根据配置文件中的初始化账号的余额,对账户状态数据库(stateDB)进行更新;二是要根据配置文件参数,生成第一个区块,并更新区块链数据库(chainDB)。
1)数据库动作一,更新账户状态数据库(stateDB):
// statedb提供了写入磁盘前的缓冲层(前面章节提到过这个特性)
statedb, _ := state.New(common.Hash{}, chainDb)
for addr, account := range genesis.Alloc {
//如果初始化json里面有账户余额预先存入的,就预先存入
address := common.HexToAddress(addr)
statedb.AddBalance(address, (account.Balance))
…
for key, value := range account.Storage {
statedb.SetState(address, (key), (value))
}
}
root, stateBatch := statedb.CommitBatch(false) //提交到数据库
内存中先用读取的json文件的配置值给struct结构赋值:
然后也会先读一下数据库,看看是否存在同hash的块,有说明创世区块已有了,会报错。
若没有则写入区块链数据库(写入过程都是先RLP编码化,再通过关键要素字符串拼接方式形成key,最后用db.Put(key, value)方式存入数据库):
图1 区块写入数据库的过程
注意,其中写入区块体时,写入的key是块号加上hash,为什么要加上块号?是为了避免hash的碰撞,如果两个区块碰巧产生了一样的hash,通过块号的区别也保证了key不同;另外分叉的时候,块号一样,但是hash就不一样了,the DAO事件后,ETH和ETC的不同,就是产生了分叉,见附录图
而普通区块的写入动作其流程是相同的,所不同的是其生成过程,创世区块很多值是通过配置文件取得值的,而普通区块是通过计算得到的。
另外,数据库中不会写入所有前台查询时展示的数据,比如区块的size: 803,这个值在go和java版本的区块定义中没有,也不会保存到数据库中占用额外体积,打印的时候计算打印出来就可以了。
读取:
说了写入,再谈一下读取。总体过程是从数据库中以block区块号为主键,读出块头,这个动作类似于关系型传统数据库根据索引找到记录。而此时账户状态数据库中已经保存的块号和头部hash之间关系就像是索引(图5,写header步骤)。
// GetHeaderByNumber retrieves a block header from the database by number,
// caching it (associated with its hash) if found.
func (self *BlockChain) GetHeaderByNumber(number uint64) *types.Header {
return self.hc.GetHeaderByNumber(number)
}
找到headerchain.go的代码中确实是按照如上二阶段流程(先读hash再读头部)读取区块头部的
func (hc *HeaderChain) GetHeaderByNumber(number uint64) *types.Header {
hash := GetCanonicalHash(hc.chainDb, number)
if hash == (common.Hash{}) {
return nil
}
return hc.GetHeader(hash, number)
}
如上是读取头部的代码,进一步地,可以读取区块头部的详细内容,获取总挖矿难度,也是之前就保存了这样的数据,所以可以查询获得难度系数:
// GetBlockNumber retrieves the block number belonging to the given hash
// from the cache or database
func (hc *HeaderChain) GetBlockNumber(hash common.Hash) uint64
另外,还可以通过编程直接读取以太坊LevelDB方式,理解以太坊后台的存储结构,以及读取的过程。
My first leveldb app!!
block reuslt: [6650a0ac6c5e805475e7ca48eae5df0e32a2147a154bb2222731c770ddb5c158]
图2 编程直接读取以太坊存储的例子
如上代码演示了如何通过编码直接读取以太坊保存的数据,程序main函数入口后,首先告诉程序数据库文件所在地址,采用api打开数据库链接,然后是用“LastBlockKey”获取最后一个区块的hash,然后根据其区块头部的parentHash再一个个往前查找,直到创世区块,可以用于区块链数据的分析。
另外要注意的是,如果客户端采用了fast syncing方法(快速同步方法),历史区块的如上部分内容就不会同步到本地数据库了,这样加快了同步速度,减少了数据体积。后续需要区块体时,根据区块头部信息从网络获取区块体信息。快速同步(fast syncing),是以太坊存储层技术的重要改进。带来更快的同步速度,更小的体积占用(相当于“臃肿”的以太坊存储数据“瘦身”)。