目录
- 介绍
- 区块链
- 世界状态
- 账户
- 交易
- 消息(Message)
- 去中心化数据库
- 原子性和顺序
- 虚拟机
- 以太坊虚拟机
- 消息调用
- 异常
- Gas和费用
- 输入和输出
- 字节指令
- 指令集
- 杂项
- 附录A: 实现
- Geth源码
- EVM开发应用
- solidity ABI
- 附录:用户界面
- Geth, mist, Solc, Remix, Truffle
- 参考
介绍
区块链
- 区块链系统实际上可以看做一个基于交易的状态机。
- 但是区块链不被称作状态链,是因为只需要有区块就能够推出每一个状态。
- 所以从区块链系统实现的角度来看,称其为区块链更加合适。
世界状态
- 世界状态中简化的存储的结构就是账户不同的账户地址和不同的账户状态。
- 这些内容就能够很好的构建整一个区块链体系。
账户
- 账户状态中是可以存储EVM代码的
- 账户又分为两种,一种是
Externally owned account(EOA)
(个人拥有账户),一种是Contract account
(合约账户)。
- 只有合约账户中存放着
EVM code
和Storage
两个组件。
- EOA由私钥控制,但是EOA不包含EVM代码。
- 合约包含EVM代码,并且由合约代码控制。
- 账户的地址来源如下图所示
交易
- 交易是一个经过加密签名的指令。
- 交易的行为都是又一个独有的个人和实体发起的。
- 交易有两种类别,一种是
contract creation
,另一种是Message call
- 在创建一个合约的时候,我们实际上执行了两步操作:
- 创建了一个地址N和一个合约状态N。
- 然后执行了合约的代码,更新了合约中的storage。
- 一个交易的结构如下图所示
信息(Message)
- 信息是在两个不同的账户之间传递的内容。
- 信息是一组数据(字节)和价值(一台)。
- 消息的传递总共只有四种情况,它们分别是
去中心化数据库
- 所有人都拥有着世界状态一个数据库,整个区块链是所有人共同拥有的,面向交易型的数据库。
- 分布式的节点构成了以太坊P2P的网络。
- 个人通过Web3 API来修改以太坊的世界状态。
原子性和顺序
- 一个交易是一个原子性的交易,它无法再分或者被破坏。
- 所以交易符合All or nothing规则,即交易要不成立,要不就不存在。
- 交易是无法被覆盖的,这说明交易必须顺序性的执行。
- 同时,交易的顺序是无法被保证的。
- 矿工可以决定区块交易的顺序:
- 此时,其他的矿工可能由不同的打包顺序,甚至包含着不同的交易数据,这时就得看最快完成PoW的矿工的选择是怎么样的。
虚拟机
以太坊虚拟机
- 以太坊虚拟机的核心逻辑如下所示
- 以太坊虚拟机是一个智能合约的运行时环境。
以太坊虚拟机架构
以太坊虚拟机空间构成
- 栈内存:1024个元素*256个比特。
- 内存:字节寻址的线性内存。
- Storage: 可持久化存储的内存。(256比特的key对应256bit的value)
栈
- 所有的操作都在栈中体现;
- 操作栈的指令有如PUSH/POP/COPY/SWAP等。
内存
- 内存是线性的并且可以在字节程度进行寻址。
- 内存的处理指令有MSTORE/MSTORE8/MLOAD等。
- 所有的内存的位置都初始化为0。
账户存储空间
- 存储空间是一个键值存储结构,他能够映射256-bit words的键值对。
- 它通过SSTORE/SLOAD指令进行接入。
- 所有的位置都被初始化为0。
以太坊虚拟机代码
执行模型
消息调用(Message call)
- EVM可以发送消息给其他的用户,其中包括合约用户和EOA。
- 信息调用的层级不能超过1024层。
- 首先EVMcode操控栈,栈再通过操控call message的指令通过arguments导入到input data,input data通过再操控EVM的内存和栈,生成新的数据,再作为返回值返回给原先的EVM。
异常
- 以太坊虚拟机中经典的异常有
错误地址
,错误指令
,燃料不足
,栈向下溢出
等。
字节顺序
Endian for Memory(内存端位)
- EVM是大端寻址,也即第N个位置对应着第LSB个位置。
- BYTE指令是
从左到右
,从MSB出发。
SIGNEXTEND
指令是从右到左
,即从LSB出发。
Push动作的字节顺序
指令集
- 有基本的256-bit的操作。
- 合约创建和销毁
- Hash
- 变换操作
- DIV操作
几种代码复制的方法
- 这里面有从input data复制到栈的
CALLDATALOAD
指令。
- 又从input data复制到内存的
CALLDATACOPY
指令。
- 从EVM code向Memory传送代码的
CODECOPY
指令。
- 有从内存复制到下一个虚拟机代码的
EXTCODECOPY
指令。
来自MUL, DIV和SDIV的移动指令
- 位向左移动可以用MUL表示。
- 位向右移动可以用DIV或者SDIV表示。
- DIV m (2^n) == m>>n
- DIV用于逻辑右移。
- SDIV用于算数右移。
杂项
- Solidity源码,Viper源码和LLL源码经过编译之后,都能够得到
EVM
的源码。
- eWASM会作为下一代的虚拟机。
一些简单的源码解析
[core/state/statedb.go]
type StateDB struct {
db Database
prefetcher *triePrefetcher
originalRoot common.Hash
trie Trie
hasher crypto.KeccakState
snaps *snapshot.Tree
snap snapshot.Snapshot
snapDestructs map[common.Hash]struct{
}
snapAccounts map[common.Hash][]byte
snapStorage map[common.Hash]map[common.Hash][]byte
stateObjects map[common.Address]*stateObject
stateObjectsPending map[common.Address]struct{
}
stateObjectsDirty map[common.Address]struct{
}
[core/state/state_object.go]
type stateObject struct {
address common.Address
addrHash common.Hash
data Account
db *StateDB
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash
CodeHash []byte
}
type Code []byte
type Storage map[common.Hash]common.Hash
栈和内存
[core/vm/stack.go]
type Stack struct {
data []uint256.Int
}
func newstack() *Stack {
return stackPool.Get().(*Stack)
}
[core/vm/memory.go]
type Memory struct {
store []byte
lastGasCost uint64
}
func NewMemory() *Memory {
return &Memory{
}
}
指令集操作
[core/vm/instruction.go]
func opAdd(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek()
y.Add(&x, y)
return nil, nil
}
func opPop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
scope.Stack.pop()
return nil, nil
}
func opMload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
v := scope.Stack.peek()
offset := int64(v.Uint64())
v.SetBytes(scope.Memory.GetPtr(offset, 32))
return nil, nil
}
func opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
mStart, val := scope.Stack.pop(), scope.Stack.pop()
scope.Memory.Set32(mStart.Uint64(), &val)
return nil, nil
}
func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
stack := scope.Stack
temp := stack.pop()
gas := interpreter.evm.callGasTemp
addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr := common.Address(addr.Bytes20())
args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
var bigVal = big0
if !value.IsZero() {
gas += params.CallStipend
bigVal = value.ToBig()
}
ret, returnGas, err := interpreter.evm.Call(scope.Contract, toAddr, args, gas, bigVal)
if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas
return ret, nil
}
创建EVM和进行交易
[core/state_processor.go]
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil {
return nil, err
}
blockContext := NewEVMBlockContext(header, bc, author)
vmenv := vm.NewEVM(blockContext, vm.TxContext{
}, statedb, config, cfg)
return applyTransaction(msg, config, bc, author, gp, statedb, header, tx, usedGas, vmenv)
}
源码调用层级
参考文档
- 以太坊黄皮书: https://ethereum.github.io/yellowpaper/paper.pdf
- 以太坊开发教程:https://github.com/ethereum/wiki/wiki/Ethereum-Development-Tutorial
- 以太坊虚拟机阐述:https://github.com/takenobu-hs/ethereum-evm-illustrated