Ethereum数据存储分析

来源:互联网 发布:奥斯曼土耳其知乎 编辑:程序博客网 时间:2024/05/18 02:15

Ethereum数据存储分析

第一部分看看geth客户端的整体结构 
创建私链的时候已经指定创世块genesis.json都放在private-geth目录下,现在是已经挖矿过的目录。

Current里面的内容是MANIFEST-000007

~

 

进入真正的存放数据的目录private-geth/data/00 
geth
中保存的是区块链的相关数据 
keystore
中保存的是该链条中的用户信息

History记录历史信息

之前我们这个节点已经创建了两个账户,现在我们可以看到keystore里面有两个账户信息的文件

每个账户都由一对钥匙定义,一个私钥和一个公钥。账户以地址为索引,地址由公钥衍生而来,取公钥的最后 20个字节。每对私钥 /地址都编码在一个钥匙文件里。钥匙文件是JSON文本文件,可以用任何文本编辑器打开和浏览。钥匙文件的关键部分,账户私钥,通常用你创建帐户时设置的密码进行加密。钥匙文件的文件名格式为UTC。账号列出时是按字母顺序排列,但是由于时间戳格式,实际上它是按创建顺序排列。如果把秘钥丢了钥匙文件可以在以太坊节点数据目录的keystore子目录下找到,接下来我们进入一个keystore目录文件看看他的信息:

 

警告:记住密码并备份钥匙文件。为了从账号发送交易,包括发送以太币,你必须同时有钥匙文件和密码。确保钥匙文件有个备份并牢记密码,尽可能安全地存储它们。这里没有逃亡路径,如果钥匙文件丢失或忘记密码,就会丢失所有的以太币。没有密码不可能进入账号,也没有忘记密码选项。所以一定不要忘记密码。

接下来进入geth可以看到chaindatalightchaindatanodes目录

进入nodes(我们这条私链有三个节点,所以这里有三个ldb文件)

进入chaindata,区块链最后的本地存储都是以ldb文件

Lightchaindata是一个轻客户端的模式,该模式无需下载较大的以太坊区块链,免去了繁琐的流程



第二部分看看源码的结构

1 Core/types/block.Go 
首先看到的是一个区块的结构

区块主要分为区块体和区块头,下面是去块体的结构,主要是交易列表和叔块列表

 

 

这是区块头的结构体

我们知道了一个区块的结构,那它是怎么存储的呢

区块和交易等数据最终都是存储在leveldb数据库中的,数据库的存储位置在datadir/geth/chaindata中,在core/database_util.go中封装了所有与区块存储和读取相关的代码,通过这些代码可以弄清楚区块、交易等数据结构在数据库中是如何存储的。

leveldb是一个key-value数据库,所有数据都是以键-值对的形式存储。key一般与hash相关,value一般是要存储的数据结构的RLP编码。区块存储时将区块头和区块体分开存储。

区块头的存储格式为:

headerPrefix + num (uint64 big endian) + hash -> rlpEncode(header)

其中key由区块头前缀、区块号(uint64大端格式)、区块hash构成,value是区块头的RLP编码。

区块体的存储格式为:

bodyPrefix + num (uint64 big endian) + hash -> rlpEncode(block body)

其中key由区块体前缀、区块号(uint64大端格式)、区块hash构成,value是区块体的RLP编码。

key中的前缀可以用来区分数据的类型,在core/database_util.go中定义了各种前缀:

headerPrefix        = []byte("h")  //headerPrefix + num (uint64 big endian) + hash -> header

tdSuffix            = []byte("t")  //headerPrefix + num (uint64 big endian) + hash + tdSuffix -> td

numSuffix           = []byte("n")  //headerPrefix + num (uint64 big endian) + numSuffix -> hash

blockHashPrefix     = []byte("H")  //blockHashPrefix + hash -> num (uint64 big endian)

bodyPrefix          = []byte("b")  //bodyPrefix + num (uint64 big endian) + hash -> block body

其中headerPrefix定义了区块头key的前缀为hbodyPrefix定义了区块体key的前缀为b

下面是存储区块头的函数:

// WriteHeader serializes a block header into thedatabase.

funcWriteHeader(db ethdb.Database, header*types.Header)error {

    data,err := rlp.EncodeToBytes(header)

   if err !=nil {

       return err

    }

    hash :=header.Hash().Bytes()

    num :=header.Number.Uint64()

    encNum:= encodeBlockNumber(num)

    key :=append(blockHashPrefix, hash...)

   if err := db.Put(key, encNum); err !=nil {

       glog.Fatalf("failed to store hash to numbermapping into database: %v", err)

    }

    key =append(append(headerPrefix, encNum...), hash...)

   if err := db.Put(key, data); err !=nil {

       glog.Fatalf("failed to store header intodatabase: %v", err)

    }

   glog.V(logger.Debug).Infof("stored header#%v [%x…]", header.Number, hash[:4])

   returnnil

}

它是先对区块头进行RLP编码,encodeBlockNumber将区块号转换成大端格式,然后组装key。这里先向数据库中存储一条 区块hash->区块号 的记录,最后将区块头的RLP编码写到数据库中。

下面是存储区块体的函数:

// WriteBody serializes the body of a block intothe database.

funcWriteBody(db ethdb.Database, hash common.Hash, numberuint64, body *types.Body)error {

    data,err := rlp.EncodeToBytes(body)

   if err !=nil {

       return err

    }

   return WriteBodyRLP(db, hash, number, data)

}

 

// WriteBodyRLP writes a serialized body of ablock into the database.

funcWriteBodyRLP(db ethdb.Database, hash common.Hash,numberuint64, rlp rlp.RawValue)error {

    key :=append(append(bodyPrefix, encodeBlockNumber(number)...), hash.Bytes()...)

   if err := db.Put(key, rlp); err !=nil {

       glog.Fatalf("failed to store block body intodatabase: %v", err)

    }

   glog.V(logger.Debug).Infof("stored blockbody [%x…]", hash.Bytes()[:4])

   returnnil

}

WriteBody先对区块体进行RLP编码,然后调用WriteBodyRLP将区块体的RLP编码写到数据库中。WriteBodyRLP根据上面的规则组装key,然后向数据库中写入一条记录。

还有一个WriteBlock函数分别调用WriteBodyWriteHeader将区块写到数据库中。此外还有GetHeader GetBody GetBlock函数用于从数据库中读取区块。

2 这是一个交易的结构体 
Core/types/transaction.
go

1ContractTransaction的区别在于:Recipient == nil

 2. Transaction能以RLP算法进行Encode和Decode;

3. hash/size/from字段是cache之用,避免多次 hash/sign导致性能损失;

交易存储

除了区块外,数据库中还存储了所有的交易,每条交易的存储格式如下:

txHash -> rlpEncode(tx)

txHash + txMetaSuffix -> rlpEncode(txMeta)

每条交易对应存储两条数据,一条是交易本身,一条是交易的元信息(meta)。交易以交易的hashkey、交易的RLP编码为value存储;元信息以txHash+txMetaSuffixkey、元信息的RLP编码为value存储。元信息中包含交易所在区块的区块hash、区块号、交易在区块中的索引。具体可以看WriteTransactions函数:

// WriteTransactions stores the transactionsassociated with a specific block

// into the given database. Beside writing thetransaction, the function also

// stores a metadata entry along with thetransaction, detailing the position

// of this within the blockchain.

funcWriteTransactions(db ethdb.Database,block *types.Block)error {

    batch:= db.NewBatch()

 

   // Iterate over each transaction and encode it with its metadata

   for i, tx :=range block.Transactions() {

       // Encode and queue up the transaction for storage

       data, err := rlp.EncodeToBytes(tx)

       if err !=nil {

           return err

        }

       if err := batch.Put(tx.Hash().Bytes(), data); err!=nil {

           return err

        }

       // Encode and queue up the transaction metadata for storage

       meta :=struct {

           BlockHash  common.Hash

           BlockIndexuint64

           Index     uint64

        }{

           BlockHash:  block.Hash(),

           BlockIndex: block.NumberU64(),

           Index:     uint64(i),

        }

       data, err = rlp.EncodeToBytes(meta)

       if err !=nil {

           return err

        }

       if err := batch.Put(append(tx.Hash().Bytes(), txMetaSuffix...), data); err !=nil {

           return err

        }

    }

   // Write the scheduled data into the database

   if err := batch.Write(); err !=nil {

       glog.Fatalf("failed to store transactionsinto database: %v", err)

    }

   returnnil

}

此外还有GetTransaction函数,根据交易hash从数据库中读取交易,它返回对应的交易、交易所在区块的区块hash、交易所在区块的区块号、交易在区块中的索引。

3Receiptroot我们刚刚在区块头有看到,那他具体包含的是什么呢?它是一个交易的结果,主要包括了poststate,交易所花费的gas,bloomlogs

4 一个个交易被打包到区块上面,那区块又是怎么变成区块链的呢? 
Core/blockchain.go

注意:1. BlockChain无结构化查询需求,仅Hash查询, Key/Value数据库最方便; 2. 低层用LevelDB存储,性能好

5 stateDB用来存储世界状态 
Core/state/statedb.go

 

 

 

 

那我们接下来看看stateObject结构体 
Core/state/state_object.go

 

注意:1.StateDB完整记录Transaction的执行情况;

 2. StateDB的重点是StateObjects

3. StateDB中的stateObjectsAccountAddress key,记录其BalancenoncecodecodeHash,以及tire中的 {string:Hash}等信息;

 

 

 

6所有的结构凑明朗了,那具体的验证过程是怎么样的呢 
Core/state_processor.go 
Core/state_transition.go 
Core/block_validator.go

StateProcessor 1. 调用StateTransition,验证(执行)Transaction 2. 计算GasReciptUncle Reward

StateTransition 
1.
验证(执行)Transaction 
3.
扣除transaction.data.payload计算数据所需要消耗的gas 
4.
vm中执行code(生成contract or 执行contract);vm行过程中,其gas会被自动消耗。如果gas不足,vm会自选退出; 
5.
将多余的gas退回到sender.balance中; 
6.
将消耗的gas换成balance加到当前env.Coinbase()中;

 

 

BlockValidator 
1.
验证UsedGas 
2.
验证Bloom 
3.
验证receiptSha 
4.
验证stateDB.IntermediateRoot

 

7  可以注意到刚才的stateblock都是写进db数据库的,那我们看一下leveldb数据库结构。Ethdb/dabase.go