本文聚焦区块链底层技术–存储技术。主要包含世界状态、账户状态和 MPT 树。 参考:https://learnblockchain.cn/books/geth/part3/statedb.html
StateDB-世界状态 从程序设计角度,StateDB 有多种用途:
维护账户状态到世界状态的映射。
支持修改、回滚、提交状态。
支持持久化状态到数据库中。
是状态进出默克尔树的媒介。
实际上** StateDB 充当 状态(数据)、 Trie(树)、 LevelDB(存储)**的协调者。
实例化 StateDB 在对状态的任何操作前,我们要先构建一个 StateDB 来操作状态。
1 2 db: = state.NewDatabase(levelDB) statedb, err := state.New(block.Root(), db)
首先,我们要告诉 StateDB ,我们要使用哪个状态。因此需要提供 StateRoot 作为默克尔树根去构建树 。StateRoot 值相当于数据版本号,根据版本号可以明确的知道要使用使用哪个版本的状态。当然,数据内容并没在树中,需要到一个数据库中读取 。因此在构建 State DB 时需要提供 stateRoot 和 db 才能完成构建。 任何实现 state.Database 接口的 db 都可以使用
1 2 3 4 5 6 7 8 9 10 11 type Database interface { OpenTrie(root common.Hash) (Trie, error) OpenStorageTrie(addrHash, root common.Hash) (Trie, error) CopyTrie(Trie) Trie ContractCode(addrHash, codeHash common.Hash) ([]byte , error) ContractCodeSize(addrHash, codeHash common.Hash) (int , error) TrieDB() *trie.Database }
通过 db 可以访问:
OpenTrie: 打开指定状态版本(root)的含世界状态的顶层树。
OpenStorageTrie: 打开账户(addrHash)下 指定状态版本(root)的账户数据存储树 。
CopyTrie: 深度拷贝树。
ContractCode:获取账户(addrHash)的合约,必须和合约哈希(codeHash)匹配。
ContractCodeSize 获取指定合约大小
TrieDB:获得 Trie 底层的数据驱动 DB ,如:** levedDB** 、内存数据库 、远程数据库
当前有两种类型的 DB 实现了 Database 接口,轻节点使用的 odrDatabase ,和正常节点端使用的带有缓存的 cachingDB 。 因为轻节点并不存储数据,需要通过向其他节点查询来获得数据,而 odrDatabase 就是这种数据读取方式的封装。一个普通节点已内置 levelDB,为了提高读写性能,使用 cachingDB 对其进行一次封装。 在实例化 StateDB 时,需要立即打开含有世界状态的 Trie 树。如果 root 对应的树不存在,则会实例化失败 ①。实例化的 StateDB 中将记录多种信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type StateDB struct { db Database trie Trie stateObjects map [common.Address]*stateObject stateObjectsDirty map [common.Address]struct {} dbErr error refund uint64 thash, bhash common.Hash txIndex int logs map [common.Hash][]*types.Log logSize uint preimages map [common.Hash][]byte journal *journal validRevisions []revision nextRevisionId int }
db: 操作状态的底层数据库 ,在实例化 StateDB 时指定 ②。
trie: 世界状态所在的树实例对象,现在只有以太坊改进的默克尔前缀压缩树。
stateObjects: 已账户地址为键的账户状态对象,能够在内存中维护使用过的账户 。
stateObjectsDirty: 标记被修改过的账户。
journal: 是修改状态的日志流水,使用此日志流水可回滚状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func New (root common.Hash, db Database) (*StateDB, error) { tr, err := db.OpenTrie(root) if err != nil { return nil , err } return &StateDB{ db: db, trie: tr, stateObjects: make (map [common.Address]*stateObject), stateObjectsDirty: make (map [common.Address]struct {}), logs: make (map [common.Hash][]*types.Log), preimages: make (map [common.Hash][]byte ), journal: newJournal(), }, nil }
上面对的代码实例化了一个 statedb。
读写 StateDB 状态 你所访问的任何数据必然属于某个账户下的状态,世界状态态仅仅是通过一颗树来建立安全的映射。因此你所访问的数据可以分为如下几种类型:
访问账户基础属性:Balance、Nonce、Root、CodeHash
读取合约账户代码
读取合约账户中存储内容
在代码实现中,为了便于账户隔离管理,使用不开放的 stateObject 来维护。 stateObject 注意代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type stateObject struct { address common.Address addrHash common.Hash data Account db *StateDB trie Trie code Code }type Account struct { Nonce uint64 Balance *big.Int Root common.Hash CodeHash []byte }
可以看到 stateObject 中维护关于某个账户的所有信息,涉及账户地址、账户地址哈希、账户属性、底层数据库、存储树等内容。 当你访问状态时,需要指定账户地址。比如获取账户合约,合约账户代码,均是通过账户地址,获得获得对应的账户的 stateObject。因此,当你访问某账户余额时,需要从世界状态树 Trie 中读取账户状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (self *StateDB) getStateObject (addr common.Address) (stateObject *stateObject) { if obj := self.stateObjects[addr]; obj != nil { if obj.deleted { return nil } return obj } enc, err := self.trie.TryGet(addr[:]) if len (enc) == 0 { self.setError(err) return nil } var data Account if err := rlp.DecodeBytes(enc, &data); err != nil { log.Error("Failed to decode state object" , "addr" , addr, "err" , err) return nil } obj := newObject(self, addr, data) self.setStateObject(obj) return obj }
state.getStateObject(addr)方法,将返回指定账户的 StateObject,不存在时 nil。 state 的 stateObject Map 中记录这从实例化 State 到当下,所有访问过的账户的 StateObject。 因此,获取 StateObject 时先从 map 缓存中检查是否已打开 ①,如果存在则返回。** 如果是第一次使用,则以账户地址为 key 从树中查找读取账户状态数据②。读取到的数据,是被 RLP 序列化过的,因此,在读取到数据后,还需要进行反序列化 ③。为了降低 IO 和在内存中维护可能被修改的 Account 信息,会将其组装成 **StateObjec ④ 存储在 State 实例 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func newObject (db *StateDB, address common.Address, data Account) *stateObject { if data.Balance == nil { data.Balance = new (big.Int) } if data.CodeHash == nil { data.CodeHash = emptyCodeHash } return &stateObject{ db: db, address: address, addrHash: crypto.Keccak256Hash(address[:]), data: data, originStorage: make (Storage), dirtyStorage: make (Storage), } }
newObject 就是将对 Account 的操作进行辅助,其中记录了账户地址、地址哈希 ⑤ 等内容,最终你读写状态都经过 stateObject 完成。
StateDB 完成持久化 在区块中,将交易作为输入条件,来根据一系列动作修改状态。 在完成区块挖矿前,只是获得在内存中的状态树的 Root 值。 StateDB 可视为一个内存数据库,状态数据先在内存数据库中完成修改,所有关于状态的计算都在内存中完成。 在将区块持久化时完成有内存到数据库的更新存储,此更新属于增量更新,仅仅修改涉及到被修改部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func (s *StateDB) Commit (deleteEmptyObjects bool ) (root common.Hash, err error) { defer s.clearJournalAndRefund() for addr := range s.journal.dirties { s.stateObjectsDirty[addr] = struct {}{} } for addr, stateObject := range s.stateObjects { _, isDirty := s.stateObjectsDirty[addr] switch { case stateObject.suicided || (isDirty && deleteEmptyObjects && stateObject.empty()): s.deleteStateObject(stateObject) case isDirty: if stateObject.code != nil && stateObject.dirtyCode { s.db.TrieDB().InsertBlob(common.BytesToHash(stateObject.CodeHash()), stateObject.code) stateObject.dirtyCode = false } if err := stateObject.CommitTrie(s.db); err != nil { return common.Hash{}, err } s.updateStateObject(stateObject) } delete (s.stateObjectsDirty, addr) } root, err = s.trie.Commit(func (leaf []byte , parent common.Hash) error { var account Account if err := rlp.DecodeBytes(leaf, &account); err != nil { return nil } if account.Root != emptyRoot { s.db.TrieDB().Reference(account.Root, parent) } code := common.BytesToHash(account.CodeHash) if code != emptyCode { s.db.TrieDB().Reference(code, parent) } return nil }) return root, err }
如上图所示,上半部分均属于内存操作,仅仅在 stateDB.Commit()时才将状态通过树提交到 leveldb 中。
MPT-默克尔压缩前缀树 是一种经过改良的、融合了默克尔树和前缀树两种树结构优点的数据结构,是以太坊中用来组织管理账户数据、生成交易集合哈希的重要数据结构。一个非叶节点存储在 leveldb 关系型数据库中,数据库中的 key 是节点的 RLP 编码的 sha3 哈希,value 是节点的 RLP 编码。想要获得一个非叶节点的子节点,只需要根据子节点的 hash 访问数据库获得节点的 RLP 编码,然后解码就行了。 以太坊有四种前缀树:
世界状态树包括了从地址到账户状态之间的映射。 世界状态树的根节点哈希值由区块保存(在 stateRoot 字段),它标示了区块创建时的当前状态。整个网络中只有一个世界状态树。
账户存储树保存了与某一智能合约相关的数据信息。 由账户状态保存账户存储树的根节点哈希值(在 storageRoot 字段)。每个账户都有一个账户存储树。
交易树包含了一个区块中的所有交易信息。 由区块头(在 transactionsRoot 区域)保存交易树的根节点哈希值。每个区块都有一棵交易树。
交易收据树包含了一个区块中所有交易的收据信息。 同样由区块头(在 receiptsRoot 区域)保存交易收据树的根节点哈希值;每个区块都有对应的交易收据树。