存储
以太坊的账户包括外部账户与合约账户,他们同用如下数据结构:
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
所有的账户数据是以一颗大的MPT进行存储,合约数据是以一颗小的MPT进行存储。也就是说,当要查找合约账户的数据时,先在账户里面找到Root字段,然后用Root字段恢复这颗小的MPT,然后在这颗小的MPT里面查找对应的数据。
以太坊预编译合约
以太坊内置了一系列预编译智能合约,主要用于在智能合约里面常用但是计算复杂的指令:比如计算哈希。到目前为止,以太坊内置了如下的的预编译合约:
var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
......
}
所以我们在创世块内置合约的时候,必须要避开这18个地址。否则调用的时候调用不到你内置的合约上面去。因为以太坊在调用EVM的时候,首先会检测是不是属于预编译地址,如果是直接执行预编译合约相关逻辑。根本不会去调用你内置的合约相关逻辑。
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
......
p, isPrecompile := evm.precompile(addr)
.......
if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
code := evm.StateDB.GetCode(addr)
if len(code) == 0 {
ret, err = nil, nil // gas is unchanged
} else {
.......
}
}
......
}
内置合约
也叫系统合约。就是将合约写入创世块里面的合约。主要用来实现一些需要启动就有的业务逻辑。比如假设共识算法采用DPOS,那么选举出块节点可以使用系统合约来实现。下面延时如何将一个合约内置到系统合约里面去。
以下面的合约 Simple.sol 为例:
pragma solidity ^0.4.25;
contract Simple {
uint256 a;
// function Simple(){
// a = 255;
// }
function set(uint256 x) public {
a = x;
}
function get() public view returns (uint256) {
return a;
}
}
使用编译器将合约编译成字节码
--bin-runtime 与 --bin 的区别:二进制 solc 编译工具同时支持 --bin-runtime 和 --bin 参数,这两个参数在编译结果上的主要差异在于:针对相同的目标Solidity 合约,使用 --bin-runtime 参数的编译字节码是 --bin 参数编译字节码的一部分。--bin 参数编译字节码除了包含--bin-runtime 参数的编译字节码结果之外,还包含合约初始化方法 constructor 的相关字节码等内容。通俗的讲 --bin = --bin-runtime + 初始化内容。
所以我们这里需要的是--bin-runtime 字节码,而不是 --bin 参数。
我们使用以太坊提供的solc-bin来进行编译。比如命令:solc --bin --bin-runtime --abi --overwrite ./Simple.sol -o ./
编译完成之后,会在当前目录生成Simple.abi,Simple.bin, Simple.bin-runtime这三个文件(我当时采用的是0.4.25版本的合约编译器)。第一个文件时合约的调用接口,第二个智能合约部署用到的内容,第三个是我们内置系统合约需要用到的东西。
生成genesis.json
将Simple.bin-runtime生成数据写入到genesis文件里面,注意记得加16进制前缀0x。一个例子如下:
{
"config": {
"chainId": 1205,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"clique": {
"period": 0,
"epoch": 30000
}
},
"nonce": "0x0",
"timestamp": "0x619dff45",
"extraData": "0x000000000000000000000000000000000000000000000000000000000000000069f4397180f27439c522cb2dab0f7d5e59ad15b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x47b76000000",
"difficulty": "0x1",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"f000000000000000000000000000000000000000": {
"balance": "100000000000000000000000000000000",
"code": "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820894ef04548f523e5a42bb2eb8c26129c0a8ea69dbf0e0e48e5ee03ad0bdb625b0029",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0xff"
}
},
"69f4397180f27439c522cb2dab0f7d5e59ad15b4": {
"balance": "100000000000000000000000000000000"
}
},
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas": null
}
创建私链
根据上面的genesis.json文件,创建私链。我们就可以在地址为0xf000000000000000000000000000000000000000
的合约上面执行交易了。为了方便调用,我将abi内容粘贴如下:
[
{
"constant": false,
"inputs": [
{
"name": "x",
"type": "uint256"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
初始化合约变量
以上面的例子,如果我想要内置的系统合约Simple.sol里面的变量a初始为255。我们不能写构造函数或者在合约里面赋值uint256 a = 255
来实现。因为构造函数理论上是合约部署完毕之后执行的一笔交易,内置的合约没有执行交易,所以构造函数不会生效。因为合约的数据最终存到levelDb里面,我们只要将合约的变量存储在levelDB里面的逻辑弄清楚了,直接将数据写入进去即可。具体请参考详解SOLIDITY合约数据存储布局。上面的Simple.sol合约里面的变量a由于存在第0个槽里面,所以我将0槽位的值直接置为0xff即可。
合约在MPT中的存储
上面说过,合约是以一颗小的MPT放在大的MPT里面,所以如果想调用合约查找合约变量里面的值,那么首先需要恢复合约账户里面的Root字段。再根据Root字段恢复合约数据存储的MPT。那么合约的数据的key-value对是如何存储的呢。在上面的“初始化合约变量里面”说过,每一个合约的变量都对应一个插槽,这个插槽就是合约数据MPT的key,而合约变量里面的值,就是对应的value。
上面的文章详解SOLIDITY合约数据存储布局从原理上分析了solidity是如何对变量的存储转为插槽与值存储。下面我们从时间角度来验证。以下面的这个简单合约StorageStringTest.sol作为说明:
pragma solidity ^0.4.25;
contract StorageStringTest {
string data = "我特别特别长,已经超过了一个插槽存储量";
function get() public view returns (string) {
return data;
}
}
首先我们将这个合约进行编译然后部署。我们在MPT读取数据的地方,将传入的key-value都打印出来。为了简单测试,我将以太坊用SecureTrie构造的地方,替换成了普通的Trie。涉及的相关代码如下所示:
将SecureTrie --> Trie
-func NewSecure(root common.Hash, db *Database) (*SecureTrie, error) {
+func NewSecure(root common.Hash, db *Database) (*Trie, error) {
if db == nil {
panic("trie.NewSecure called without a database")
}
if err != nil {
return nil, err
}
- return &SecureTrie{trie: *trie}, nil
+ return trie, nil
}
打印账户信息:
func toAccount(value []byte) (*Account, error) {
data := new(Account)
err := rlp.DecodeBytes(value, data)
return data, err
}
func (t *Trie) decodeAccount(key, value []byte) {
data, err := toAccount(value)
if err == nil {
log.Info("decodeAccount right",
"key", hexutils.BytesToHex(key),
"nonce", data.Nonce,
"balance", data.Balance.String(),
"Root", data.Root.String(),
"CodeHash", hexutils.BytesToHex(data.CodeHash),
)
} else {
log.Info("decodeAccount contract",
"key", hexutils.BytesToHex(key),
"value", hexutils.BytesToHex(value),
)
}
}
func (trie *Trie) TryGet(key []byte) ([]byte, error) {
value, newroot, didResolve, err := trie.tryGet(trie.root, keybytesToHex(key), 0)
if err == nil && didResolve {
trie.root = newroot
}
t.decodeAccount(key, value)
return value, err
}
上面的decodeAccount是将传进来的key从MPT里面读出来的value进行解码,如果能进行解码,那么证明是读取的账户数据。如果不能解码,那么读取的是合约数据账户。调用StorageStringTest.sol合约的get函数,打印的信息如下:
decodeAccount right key=8AC97D4C09DB7F957D6F9A5264C2D82AFED21FAC nonce=1 balance=0 Root=0x2dacbe64c90437696ab5e194b6232d0bd6167a73774f9ee2dfff81e1a142a0b3 CodeHash=41B31DF8CAC60D88DAA158FAAE5E7D90A0F3FA2956E12FD2460AA5021562BDAA
decodeAccount contract key=0000000000000000000000000000000000000000000000000000000000000000 value=73
decodeAccount contract key=290DECD9548B62A8D60345A988386FC84BA6BC95484008F6362F93160EF3E563 value=A0E68891E789B9E588ABE789B9E588ABE995BFEFBC8CE5B7B2E7BB8FE8B685E8BF
decodeAccount contract key=290DECD9548B62A8D60345A988386FC84BA6BC95484008F6362F93160EF3E564 value=A087E4BA86E4B880E4B8AAE68F92E6A7BDE5AD98E582A8E9878F00000000000000
解析一下相关打印信息:
第一行,能解码出来一个正常的账户,因为首先要读取合约信息,拿到Root恢复合约信息对应的MPT。
第二行,第三行,第四行,就是对合约数据的存储了。注意,对于value值,是经过RLP编码的数值,所以我们需要对其进行先解码才是storage的信息。比如RLP.decode(A0E68891E789B9E588ABE789B9E588ABE995BFEFBC8CE5B7B2E7BB8FE8B685E8BF)为0xe68891e789b9e588abe789b9e588abe995bfefbc8ce5b7b2e7bb8fe8b685e8bf。如果不想进行RLP解码,那么可以使用web3.eth.getStorageAt接口拿到的value值就是解码过后的数据。
当然,这是在知道槽信息的情况下可以这么获取值。也可以使用MPT里面的迭代器iterator.go,利用合约的Root,将合约存储在MPT里面的数据都便利出来。比如要取到某个root为"0x94b88b225..."合约下面所有的storage里面存储的数据,那么可以进行如下编码拿到。注意,如果使用SecureTrie,那么迭代后的key为hash(key),为了方便查看,在部署链之前,你可以将SecureTrie中的hashKey改为直接返回key。
func (t *SecureTrie) hashKey(key []byte) []byte {
return key // 直接返回key,原本是要返回 hash(key)的
}
// 为了方便获取,部署合约完毕将合约中想要的初始变量值发送交易初始完毕(或者直接在构造函数里面初始化),直接在core/state/database.go的OpenTrie里面写迭代这颗树的storage去获取
// OpenTrie opens the main account trie at a specific root hash.
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
tr, err := trie.NewSecure(root, db.db)
t, _ := trie.New(common.HexToHash("0x94b88b225d286e94802c7dd79e7700f60d0542e3c47ef726efa9ebc4bca2334a"), db.db)
it := trie.NewIterator(t.NodeIterator(nil))
for it.Next() {
value := make([]byte, 0, 32)
rlp.DecodeBytes(it.Value, &value)
log.Info("contract storage",
"key", hexutils.BytesToHex(it.Key),
"rlp value", hexutils.BytesToHex(it.Value),
"decode value", hexutils.BytesToHex(value))
}
if err != nil {
return nil, err
}
return tr, nil
}
我们拿到这些信息,可以将他作为合约的storage来进行验证。那么对应的genesis合约数据如下:
{
"1000000000000000000000000000000000000000": {
"balance": "0x1",
"code": "0x608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636d4ce63c14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561016e5780601f106101435761010080835404028352916020019161016e565b820191906000526020600020905b81548152906001019060200180831161015157829003601f168201915b50505050509050905600a165627a7a723058207295c4f67b1a60b9e2e3b0041e41c17cd04ef2b68189003f70bd31b2f14497650029",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x73",
"0x290DECD9548B62A8D60345A988386FC84BA6BC95484008F6362F93160EF3E563": "0xe68891e789b9e588abe789b9e588abe995bfefbc8ce5b7b2e7bb8fe8b685e8bf",
"0x290DECD9548B62A8D60345A988386FC84BA6BC95484008F6362F93160EF3E564": "0x87e4ba86e4b880e4b8aae68f92e6a7bde5ad98e582a8e9878f00000000000000"
}
}
}
用这些合约数据创建创世块,链启动之后,直接调用确实就拿到了合约中data变量的数据。
合约调用更新栈
core/state.(*stateObject).updateRoot at state_object.go:389
core/state.(*StateDB).IntermediateRoot at statedb.go:853
consensus/clique.(*Clique).Finalize at clique.go:573
consensus/clique.(*Clique).FinalizeAndAssemble at clique.go:581
miner.(*worker).commit at worker.go:1021
miner.(*worker).commitNewWork at worker.go:1012
miner.(*worker).mainLoop at worker.go:530
上面对应的智能合约数据如下:solidity