主要涉及代码
- trie
- core
- core/rawdb
- core/state
- ethdb/leveldb
需要掌握的知识点
-
StateDB Trie Account stateObject
-
账户数据如何存储在Trie中
- 账户数据的存储是使用SecureTrie中的,主要是使用keecak512(key)作为真实的key存在levelDB中。
- 由于key为[]byte类型,范围为0 ~ 255。为了fullNode数组长度不会太大浪费空间,会将key拆成nibble(以太坊定义一个nibble == 二分之一个byte),这样将数组定义的长度256将为16个(数组最后一个用来存值)。将key拆成nibble的过程中,在最后加了一个结尾符号16,其主要目的是为了区分扩展节点与叶子节点。比如分别将key1:646f65t,key2:646f67t作为key插入。第一次key1:646f65t为叶子节点,再插入key2则以key1,key2的公共key:646f6作为扩展节节点,val为分支节点。其主要作用是这样设计在内存方便访问。
func keybytesToHex(str []byte) []byte { l := len(str)*2 + 1 var nibbles = make([]byte, l) for i, b := range str { nibbles[i*2] = b / 16 nibbles[i*2+1] = b % 16 } nibbles[l-1] = 16 return nibbles }
- nonce, balance, mptRoot, codeHash 以RLP形式编码作为value存在Trie中
-
账户数据在Trie中的更新
-
如何将Trie存到levelDB中去。 trie.Commit
- 使用多线程将整棵树未计算的哈希计算出来。trie.Hash()
- 将需要提交的数据进行处理,主要是计算节点的数据大小,key由nibble转byte,处理完成之后将节点数据转为leaf通过通道的方式发动到内存数据库,内存数据库缓存过来的数据以及更新脏值相关信息。committer.commit(), committer.store(),database.insert()
- 内存数据库将发送过来的值缓存到内存中。committer.commitLoop()
- 每出一个新快,执行数据库的引用与解引用(为了释放Trie树?),执行数据库的写操作最终落盘。直接进行写入:database.Commit()。根据条件比如是超过了缓存在数据库中规定的大小进行写入:database.Cap()
INFO [08-04|15:35:18.248] Imported new chain segment blocks=66 txs=7350 mgas=510.504 elapsed=8.028s mgasps=63.587 number=7,784,332 hash=502762..81558d age=2y2mo4w dirty=1.29GiB
INFO [08-04|15:35:46.992] Imported new chain segment blocks=28 txs=3249 mgas=199.757 elapsed=28.744s mgasps=6.949 number=7,784,360 hash=cbaf6e..4e2f11 age=2y2mo4w dirty=1.29GiB
INFO [08-04|15:36:13.773] Imported new chain segment blocks=1 txs=127 mgas=7.999 elapsed=26.781s mgasps=0.299 number=7,784,361 hash=89e47e..d59274 age=2y2mo4w dirty=1.29GiB
INFO [08-04|15:36:16.295] Deep froze chain segment blocks=212 elapsed=19.035s number=7,694,360 hash=e06e45..9d1dcb
WARN [08-04|15:51:24.291] Checkpoint challenge timed out, dropping id=45c593da62c28562 conn=staticdial addr=220.249.16.230:30303 type=Geth/v1.10.3-unstabl...
WARN [08-04|15:52:06.513] Checkpoint challenge timed out, dropping id=45c593da62c28562 conn=staticdial addr=220.249.16.230:30303 type=Geth/v1.10.3-unstabl...
WARN [08-04|15:56:06.553] Served eth_coinbase conn=172.16.0.117:64612 reqid=725 t="27.802µs" err="etherbase must be explicitly specified"
WARN [08-04|15:56:09.695] Served eth_coinbase conn=172.16.0.117:56266 reqid=846 t="21.357µs" err="etherbase must be explicitly specified"
WARN [08-04|15:56:10.500] Served eth_coinbase conn=172.16.0.117:56266 reqid=850 t="36.374µs" err="etherbase must be explicitly specified"
INFO [08-04|15:56:27.571] Persisted trie from memory database nodes=5,832,133 size=1.20GiB time=20m22.96008813s gcnodes=31,170,856 gcsize=12.01GiB gctime=2m25.98558795s livenodes=231,435 livesize=-255228038.00B
// trie/trie.go 517
// 注意:
// SecureTrie 的 kv 对
// key: "secure-key-" + keecak256(key)
// value: key
func (t *Trie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
...
if db.preimages != nil {
rawdb.WritePreimages(batch, db.preimages) // 将 key 与 keecak512(key) 写入到leveldb中
}
...
}
// trie/trie.go 517
// 这里将nodeFlag即key:hash(node), value:RLP([node.key, node.value])存入数据库中。为了节省空间,node.key是使用将nibble转为byte即调用hexToCompact后的key。
func (db *Database) commit(hash common.Hash, batch ethdb.Batch, uncacher *cleaner, callback func(common.Hash)) error {
...
rawdb.WriteTrieNode(batch, hash, node.rlp()) // 将 keecak256(key) 与 节点数据写入到leveldb中
...
}
-
如何将数据从levelDB中读取出来恢复到Trie中
// trie/trie.go 499 func (t *Trie) resolveHash(n hashNode, prefix []byte) (node, error) { hash := common.BytesToHash(n) if node := t.db.node(hash); node != nil { return node, nil } return nil, &MissingNodeError{NodeHash: hash, Path: prefix} }
-
MPT的证明
开发模式的genesis配置
core/genesis.go 415
func DeveloperGenesisBlock(period uint64, faucet common.Address) *Genesis {
// Override the default period to the user requested one
config := *params.AllCliqueProtocolChanges
config.Clique = ¶ms.CliqueConfig{
Period: period,
Epoch: config.Clique.Epoch,
}
// Assemble and return the genesis with the precompiles and faucet pre-funded
return &Genesis{
Config: &config,
ExtraData: append(append(make([]byte, 32), faucet[:]...), make([]byte, crypto.SignatureLength)...),
GasLimit: 11500000,
BaseFee: big.NewInt(params.InitialBaseFee),
Difficulty: big.NewInt(1),
Alloc: map[common.Address]GenesisAccount{
common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
common.BytesToAddress([]byte{3}): {Balance: big.NewInt(1)}, // RIPEMD
common.BytesToAddress([]byte{4}): {Balance: big.NewInt(1)}, // Identity
common.BytesToAddress([]byte{5}): {Balance: big.NewInt(1)}, // ModExp
common.BytesToAddress([]byte{6}): {Balance: big.NewInt(1)}, // ECAdd
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
faucet: {Balance: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(9))},
},
}
}
状态数中账户结构
core/state/state_object.go 102
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // Merkle Patricia Tree 的根节点Hash值,默认空值。
CodeHash []byte
}
- Nonce:如果是外部账户,nonce代表从此账户地址发送的交易序号。如果是合约账户,nonce代表此账户创建的合约序号
- Balance:此地址拥有Wei的数量, 1Ether=10^18Wei
- Root: Merkle Patricia Tree 的根节点Hash值,默认空值。
- CodeHash:此账户EVM代码的hash值。对于合约账户,就是被Hash的代码;对于外部拥有账户,c是一个空字符串的Hash值
MPT 空节点计算方式
emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
keccak256(RLP.encode("")) ==> keccak256([0x80]) ==> "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
MPT 中 hashRoot 计算
x 1 x x 4 5 6 x 8 x 10 x x 13 14 15 x
|
x x 2 x x x x 7 x x x x x x x x x
H1 = HASH( RLP(1:KV) )
H2 = HASH( RLP(4->2:KV) )
H3 = HASH( RLP(4->7:KV) )
H4 = HASH( RLP([NIL, NIL, H2, NIL, ..., H3, ...., NIL]) )
...
RootHash = HASH( RLP([NIL, H1, NIL, NIL, H4, ..., HASH(RLP(15:KV)), NIL]) )
进行hashRoot的时候,会将nibble恢复成byte进行哈希计算。其主要原因是为了节省空间存在levelDB中。其大概流程是:首先去掉结尾标识符。如果有结尾标识符,则存在第1个byte的第从左往右边第6个位(即代码的左移5个位)。如果nibble的长度是个奇数,首先在第一个byte的第5个位职位1作为标记符。然后将第一个nibble复制第一个byte的后四位。如果是偶数,则第一个byte的后4位置为0。代码如下:
func hexToCompact(hex []byte) []byte {
terminator := byte(0)
if hasTerm(hex) {
terminator = 1
hex = hex[:len(hex)-1]
}
buf := make([]byte, len(hex)/2+1)
buf[0] = terminator << 5 // the flag byte
if len(hex)&1 == 1 {
buf[0] |= 1 << 4 // odd flag
buf[0] |= hex[0] // first nibble is contained in the first byte
hex = hex[1:]
}
decodeNibbles(hex, buf[1:])
return buf
}
一个细节:如果 RLP 编码后的数据小于 32 byte, 则不做哈希直接返回。但是为了保住跟哈希能算,根哈希计算小于32byte也会强制计算。
levelDB 中的key对
说明 | key | value | 备注 |
---|---|---|---|
账户数据 | "a" + keecak256(address) | RLP([nonce, balance, root, code]) | key 十六进制以 61开头 |
SecureTrie | "secure-key-" + keecak256(key) | key | key 十六进制以 7365637572652d6b65792d |
Trie | keecak256(RLP(hexToCompact(key), value)) | RLP([hexToCompact(key),value]) | 注意RLP(key, value)中的key进行了 hexToCompact 压缩 |
合约Code | "c" + keecak256(code) | code | key 十六进制以 63 |
最新的区块头 | "LastHeader" | hash | 最新的区块头 HeaderChain中使用 |
最新的区块头 | "LastBlock" | hash | 最新的区块头 BlockChain中使用 |
最新的区块头 | "LastFast" | hash | 最新的区块头 BlockChain中使用 |
区块链的高度 | "h"+num+"n" | hash | 用来存储规范的区块链的高度和区块头的hash值 |
总难度 | "h" + num + hash + "t" | td | 总难度 |
区块的高度 | "H" + hash | num | 区块的高度 在HeaderChain中使用 |
区块数据 | "b" + num + hash | block body | 区块数据 |
区块收据 | "r" + num + hash | block receipts | 区块收据 |
交易hash | "l" + txHash | hash,num,TxIndex | 交易hash可以找到区块和交易 |
levelDB相关
#include <iostream>
#include <sstream>
#include <string>
#include <chrono>
#include "leveldb/db.h"
using namespace std;
int main(int argc, char **argv)
{
// Set up database connection information and open database
leveldb::DB *db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "./testdb", &db);
if (false == status.ok())
{
cerr << "Unable to open/create test database './testdb'" << endl;
cerr << status.ToString() << endl;
return -1;
}
{
auto start = std::chrono::steady_clock::now();
leveldb::WriteOptions writeOptions;
for (unsigned int i = 0; i < 1000000; ++i)
{
ostringstream keyStream;
keyStream << "012345678901234567890123456789" << i;
ostringstream valueStream;
valueStream << "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
db->Put(writeOptions, keyStream.str(), valueStream.str());
}
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::micro> elapsed = end - start;
cout << "write 100w :" << (double)elapsed.count() / 1000000 << "s" << endl;
}
{
auto start = std::chrono::steady_clock::now();
std::string val = "";
for (unsigned int i = 0; i < 1000000; ++i)
{
ostringstream keyStream;
keyStream << "012345678901234567890123456789" << i;
db->Get(leveldb::ReadOptions(), keyStream.str(), &val);
}
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::micro> elapsed = end - start;
cout << "read 100w :" << (double)elapsed.count() / 1000000 << "s" << endl;
}
// Close the database
delete db;
}
write 100w :4.30719s (100 / 4.30 == 23.2w)
read 100w :1.79004s (100 / 1.79 == 55.8w)
read 100w :1.63528s (key值不存在)
官网
fillseq : 1.765 micros/op; 62.7 MB/s
fillsync : 268.409 micros/op; 0.4 MB/s (10000 ops)
fillrandom : 2.460 micros/op; 45.0 MB/s
overwrite : 2.380 micros/op; 46.5 MB/s
1 / 2.460 == 40.6w
readrandom : 16.677 micros/op; (approximately 60,000 reads per second)
readseq : 0.476 micros/op; 232.3 MB/s
readreverse : 0.724 micros/op; 152.9 MB/s
1 / 16.677 == 5.99w
depth == 20,仅仅读取出来,2 ^ 21 / 1000000 * 1.79 == 3.75s,还有RLP解码。
--dev 模式 chainId
params/config.go
AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}}
开发进度
已完成的开发
- 二叉默克尔树的定义
- 二叉默克尔树插入(更新)
- 二叉默克尔树删除
- 二叉默克尔树修改
- 二叉默克尔树的哈希计算
- 二叉默克尔树的哈希节点采用mmap内存映射方式实现
- 持久化到硬盘中
- 从硬盘恢复到内存中
- 二叉默克尔树持久化到levelDB中
- 持久化每次只持久化修改的部分
- 叶子节点从levelDB恢复到内存中
- 读取
- 解码
- 在内存中跑通一个节点,对MPT的修改只保存在内存中,没有持久化到leveldb中
core/state/database.go 131 func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) { tr, err := trie.NewSecure(root, db.db) //tr, err := trie.NewBinary(root, db.db, 1) if err != nil { return nil, err } return tr, nil }
- 将上层调用初始化MTP的代码,替换成使用修改过后的BinaryMPT。因为代码逻辑有多处根据stateRoot来初始化MPT,改为单例模式初始化BinaryMPT。
- 二叉默克尔树与状态数据库之间的交互(将MPT持久化到leveldb中)
- 持久化地方
- 创世区块
- 出块:跑通一个节点,使用 --gcmode archive --cache.snapshot 0 模式将修改过后的数据能持久化到levelDB中。但是还有点小问题,RLP编解码有问题,存进去的数据有部分还无法解码(应该是节点编码有问题)。如:A7640000A056E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421A0C5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470A470A470A470A470
- 调通智能合约的调用(BMPT与MPT的底层数据库共用同一个,所以在调用数据库逻辑的时候,需要区分是BMPT还是MPT)
- 合约数据的查询
-
合约数据的持久化
- 完善BinaryMPT单例的模式
- 将BMPT里面实例化的以前的单例模式去掉,改为树形结构使用单例。为可以提供多颗BMPT做好准备
-
实现在内存中的历史数据查询,即实现了对纪录的修改保存。
type DiffLeaf struct { Index uint32 Leaf binaryLeaf } type Alter struct { PreRoot [32]byte CurRoot [32]byte DiffLeafs []DiffLeaf } binaryRoot := t.binaryRoot() // 当前的最新root trieRoot := t.trieRoot() // 初始化的root // 这是要查历史数据 if binaryRoot != trieRoot { for _, alter := range t.bt.alters { if alter.PreRoot == trieRoot { for _, diffLeaf := range alter.DiffLeafs { if diffLeaf.Index == uint32(leafIndex) { return diffLeaf.Leaf } } } } leaf = make([]binaryNode, 0, 0) }
- 实现将每个区块对应的修改值进行RLP编码并存到leveldb
-
完成从leveldb加载每个区块对应的修改值并进行RLP解码还原到内存中
// f90340a05a013a87733553966400242399dee3760877fead2cd87287747155e47a854acba050fe07922f57ae3b4553201bfd7c11aca85e1541f91db8e62dca9c418dc5feaef902fbf9015102f9014df87d94b44719dc13fc46773e8cfcf79fa2f1ef2e8dcc57b866f86401a0fffffffffffffffffffffffffffffffffffffffffffffffa943935b75e584a37a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f86594c08c699d71038eb9a2e6bb1f4139017887dc2fe8b84ef84c808853444835ec580000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f86594c2bcdc28760bda2315c77a3587e47c9c996c6d27b84ef84c808853444835ec580000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f8d201f8cff866945e17555c1ed3bb5010aa9386d56ca823a0f33d19b84ff84d068903e7327d55c1a44000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f865946af797299b61969e2a1b5a35c605cd7216b5d119b84ef84c80881bc16d674ec80000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f8d103f8cef86594cf7e747bec87dd652c9eef91645de965b481a241b84ef84c80886124fee993bc0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470f86594dea01d44ae5a38dcf83425605b8d587bf93ebb28b84ef84c808829a2241af62c0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 // alters retrieves a alters from leveldb func (db *Database) resolveAlters(hash common.Hash) []Alter { alters := make([]Alter, 0, 0) root := hash count := 0 for { count++ cur := rawdb.ReadAlters(db.diskdb, root) if len(cur) == 0 { break } alter := Alter{} elems, rest, err := rlp.SplitList(cur) if err != nil { } cur = elems elems, rest, err = rlp.SplitString(cur) copy(alter.PreRoot[0:], elems) cur = rest elems, rest, _ = rlp.SplitString(cur) copy(alter.CurRoot[0:], elems) cur = rest elems, rest, err = rlp.SplitList(cur) cur = elems for { var diffLeaf DiffLeaf index := make([]byte, 0, 0) next := make([]byte, 0, 0) elems, rest, err = rlp.SplitList(cur) next = append(next, rest...) cur = next index, rest, err = rlp.SplitString(elems) diffLeaf.Index = uint32(bytesToInt(index)) elems, rest, err = rlp.SplitList(rest) leaf := make([]binaryNode, 0, 0) // 如果叶子节点是空的,默认为空数组 cur := elems for { var node binaryNode key := make([]byte, 0, 0) val := make([]byte, 0, 0) elems, rest, err = rlp.SplitList(cur) next := rest cur = rest if len(elems) > 0 { elems, rest, err = rlp.SplitList(elems) key, rest, err = rlp.SplitString(rest) val, rest, err = rlp.SplitString(rest) node.Key = key node.Val = val leaf = append(leaf, node) } if len(next) == 0 { break } } diffLeaf.Leaf = leaf alter.DiffLeafs = append(alter.DiffLeafs, diffLeaf) if len(next) == 0 { break } } alters = append(alters, alter) copy(root[:], alter.PreRoot[:]) // 最多支持回滚10个 if count >= 10 { break } } return alters }
正在开发中
- 持久化到硬盘中的条件判断
- 以当前未写入链上的数据大小之和为条件进行判断,比如大于10兆进行写一次。
- 根据区块个数进行判断,比如每100个区块进行写入一次
- 对levelDB数据进行清理
- 保留10个历史区块数据(目前按10个做,可做为一个可配参数)
- 每出10个区块,清理内存中前10个区块的历史数据
- 每进行一次持久化,清理上一次写入leveldb中的数据
- 区块的回滚
- 链分叉回滚
- 因为链挂掉或强制停止链等情况导致数据不一致的链回滚
待开发的功能
- 叶子节点每个元素分开存储(优化功能)
- 二叉默克尔树迭代器
- 二叉默克尔树的证明
开发备注
- 如果构建的时候将整个哈希节点从leveldb加载进来,是否加载最后一层然后再计算比从leveldb中加载要快。