以太坊智能合约相关

存储

以太坊的账户包括外部账户与合约账户,他们同用如下数据结构:

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

暂无评论

发送评论 编辑评论


				
上一篇
下一篇