以太坊学习1

1、Ethereum代码里用到了大量的hash函数。算法中,hash函数是不可逆的。例如:h=hash(x)只能通过x算出h的值,不能通过h来逆推x的值。所以可以唯一地作某个对象的全局唯一标识符。
2、Ethereum中的hash函数全部采用SHA-3的标准。
3、RLP(Recursive Length Prefix(递归长度编码))。其作用是将任意字节数组([]byte)展平成无嵌套的字节数组。即将多维的数据变成一维的。RLP是可逆的,它提供了互逆的编码、解码方法。
4、总结来说,Ethereum中具体使用的hash算法实际上是对某个类型对象的RLP编码值做了SHA3哈希运算(RLP Hash)。Ethereum在底层的存储中特意选择了专门的[k,v]数据库。其中,v表示某个结构对象的RLP编码值(byte[]),k大多情况下为v的RLP编码后的SHA-3哈希值。大致的顺序如下:
任意嵌套的byte[]----->(RLP)平铺byte[]----->(SHA3+HASH)--------->v值.
5、big.int是golang提供的数据类型,用来处理比较大的整型数,也可以处理正常的数。

# /go-1.x/src/math/big/int.go
package big
type Int struct {
    neg bool  // sign, whether negaive
    abs nat   // absolute value of integer
}

根据big.Int的结构定义,它实一个结构体,所以在每次新建big.Int时可以用x:=new(big.Int),返回一个指针。在Ethereum中比较常用big.Int是Gas(汽油)和Ether(比特币).
6、Gas是Ethereum里对所有活动进行消耗资源计量的单位。Ether即Ethereum中使用的数字货币即以太币。
如果Address A要向Adddress B发一笔金额 C,A除了要含有>=C的Ether外还要有额外的金额用以支付交易所消耗的Gas。
7、区块(Block)是Ethereum的核心结构体之一,是交易的集合。BlockChain(区块链)是一个个Block以单向链表的形式相互关联起来的。每个Block中都带有一个Header(指针)。

# /core/types/block.go
type Block struct {
    header *Header
    transactions Transactions  // type Transactions []*Transaction
    ...
}
type Header struct {
    ParentHash common.Hash
    Number *big.Int
    ...
} 

1)Header结构体带有Block的所有属性信息,其中的ParentHash 表示该区块的父区块哈希值, 亦即Block之间关联起来的前向指针。只不过要想得到父区块(parentBlock)对象,直接解析这个ParentHash是不够的, 而是要将ParentHash同其他字符串([]byte)组合成合适的key([]byte), 去kv数据库里查询相应的value才能解析得到。
2)Header结构体中的Number表示该区块在整个区块链中的位置,每一个区块相对于其父区块其Number+1,即整个区块链都会有一个原始区块,即创世块(GenesisBlock)其Number的值为0。
3)Block中有一个Tranction数组。Tranction简称tx,是Ethereum里标示一次交易的结构体,如下:

# /core/types/transaction.go
type Transaction struct {
    data txdata
    hash, size, from atomic.Value  // for cache
}
type txdata struct {
    AccountNonce uint64
    Price *big.Int
    GasLimit *big.Int
    Recipient *common.Address
    Amount *big.Int
    Payload []byte
    V, R, S *big.Int   // for signature
    Hash *common.Hash  // for marshaling
}

1)每个tx都声明了自己的Gas Price和GasLimit.前者表示的是单位Gas消耗所折抵的Ether多少。后者表示的是该tx执行过程中所允许消耗资源的上限。每个tx都拥有独立的Price和GasLimit,所以每个tx的执行都是相互独立的。
2)Recipient表示转账转入方的地址,可能为空(nil)。
3)Payload是一个数组,既可以是指令数组也可以是数据数组。
4)在交易的定义里是不存在转账转出方的地址的,并不是没有而是加密隐藏了,所以不会显示出来。这个加密的过程在Ethereum中被称为签名(sign).
整个合约由以太坊虚拟机(Ethereum Virtual Machine,EVM)创建并执行。

8、Block类型的基本目的之一是为了执行交易。Ethereum里采用的是广义的交易概念。分为内外两层结构:第一层是虚拟机外,包括执行前将Transaction类型转化成Message,创建虚拟机(EVM)对象,计算一些Gas消耗,以及执行交易完毕后创建收据(Receipt)对象并返回等;第二层是虚拟机内,包括执行转帐,和创建合约并执行合约的指令数组。
执行tx的入口函数是StateProcessor的Process()函数,其实现代码如下:

# /core/state_processor.go
func (p *StateProcessor) Process(block *Block, statedb *StateDB, cfg vm.Config) (types.Receipts, []*types.Log, *big.Int, error) {
    var {
        receipts     types.Receipts
        totalUsedGas = big.NewInt(0)
        header       = block.Header()
        allLogs      []*types.Log
        gp           = new(GasPool).AddGas(block.GasLimit())
    }
    ...
    for i, tx := range block.Transactions() {
        statedb.Prepare(tx.Hash(), block.Hash(), i)
        receipt, _, err := ApplyTransaction(p.config, p.bc, author:nil, gp, statedb, header, tx, totalUsedGas, cfg)
        if err != nil { return nil, nil, nil, err}
        receipts = append(receipts, receipt)
        allLogs = append(allLogs, receipt.Logs...)
    }
    p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles(), receipts)
    return receipts, allLogs, totalUsedGas, nil
}

1)Gaspool的类型是big.Int。它的作用是显示剩下的可以使用的Gas值。在每个tx的执行过程中,Ethereum还设计了偿退(refund)环节,所偿还的Gas数量也会加到GasPool里。
2)Process()的核心是一个for循环,它将Block里的所有tx逐个遍历执行,每次都会返回一个收据(Receipt)收据对象。
3)Receipt 中有一个Log类型的数组,其中每一个Log对象记录了Tx中一小步的操作。所以,每一个tx的执行结果,由一个Receipt对象来表示;更详细的内容,由一组Log对象来记录。这个Log数组很重要,比如在不同Ethereum节点(Node)的相互同步过程中,待同步区块的Log数组有助于验证同步中收到的block是否正确和完整,所以会被单独同步(传输)。
4)这里Receipt的Bloom,被用以验证某个给定的Log是否处于Receipt已有的Log数组中。
StateProcessor.ApplyTransaction()的具体实现,它的基本流程如下图:
在这里插入图片描述
1)ApplyTranction()首先根据输入参数分别封装出一个Massage对象和一个EVM对象,加上一个传入的GasPool类型的变量,由TransitionDb()函数完成tx的执行,执行完毕后,创建一个receipt对象,最后返回该对象以及tx执行过程中所消耗的Gas数量。
2)GasPool对象在一个Block执行开始时创建,并在该Block内所有tx的执行过程中共享。Message由此次待执行的tx对象转化而来,并有解析出tx的转账转出地址,属于待处理对象。EVM作为此次tx的执行者,完成转账和合约的相关操作。
3)TransitioinDb()的执行过程(/core/state_transition.go)。假设有StateTransition对象st, 其成员变量initialGas表示初始可用Gas数量,gas表示即时可用Gas数量,初始值均为0,于是st.TransitionDb() 可由以下步骤展开:
1)):购买Gas,从交易的转出方账户扣除一笔Ether,费用等于tx.data.GasLimittx.data.Price;同时st.initalGas = st.gas = tx.data.GasLimit,
(GasPool)gp-=st.gas
2)):计算tx固有的Gas消耗–intrinsicGas。它分为两个部分,每一个tx预设的消耗量,这个消耗量还因tx是否含有(转帐)转入方地址而略有不同;以及针对tx.data.Payload的Gas消耗,Payload类型是[]byte,关于它的固有消耗依赖于[]byte中非0字节和0字节的长度。最终,st.gas -= intrinsicGas
3)):EVM的执行。如果交易的转账转入方地址(tx.data.Recipient)为空,调用EVM的Create函数;否则调用Call()函数。无论哪个函数返回都更新st.gas。
4))计算本次执行交易的实际Gas消耗:requireGas = st.initialGas - st.gas
5))偿退Gas。它包括两个部分:首先将剩余的st.gas折算成Ether,归还交易的转出账户。之后基于实际的消耗量requireGas,系统提供一定的补偿,数量为refundGas。refundGas所折算的Ether会被立即加在转出账户上,同时st.gas+=refundGas,gp+=st.gas,即剩余的Gas加上系统补偿的Gas,被遗弃归并进GasPool,供之后的交易执行使用。
6))奖励所属区块的挖掘者:系统给所属区块的作者,亦即挖掘者账户,增加一笔金额,数额等于 st.data,Price * (st.initialGas - st.gas)。注意,这里的st.gas在步骤5中被加上了refundGas, 所以这笔奖励金所对应的Gas,其数量小于该交易实际消耗量requiredGas。
9、Ethereum中每个交易tx对象在被放进block时,都是经过数字签名的,目的是可以在后续传输和处理中随时验证tx是否被篡改。它采用椭圆曲线数字签名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)此算法的优点是在提供相同安全级别的同时仅需要更短的公钥。值得注意的是,tx的转账转出地址,就是该tx对象ECDSA签名计算时所用的公钥publicKey。
10、Ethereum中的数字签名计算过程所生成的签名(signature), 是一个长度为65bytes的字节数组,它被截成三段放进tx中,前32bytes赋值给成员变量R, 再32bytes赋值给S,末1byte赋给V,当然由于R、S、V声明的类型都是
big.Int, 上述赋值存在[]byte -> big.Int的类型转换。
在这里插入图片描述
当需要回复tx对象的转账转出方地址时,Ethereum会先从tx的signature中恢复公钥,再将公钥转化成一个common.Address类型的地址,signature由tx对象的三个成员变量R,S,V转化成字节数组[]byte后拼接得到。
生成数字签名的函数叫SignTx(),它会先调用其他函数生成signture,然后调用tx.WithSignature()将signature分段赋值给tx的成员变量R,S,V。

func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error)

接口Signer, 用来执行挂载签名,恢复公钥,对tx对象做哈希等操作。

// core/types/transaction_signing.go
type Signer innterface {
    Sender(tx *Transaction) (common.Address, error)
    SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error)
    Hash(tx *Transaction) common.Hash
    Equal(Signer) bool
}

恢复出转出方地址的函数叫Sender(), 参数包括一个Signer, 一个Transaction,代码如下:

func Sender(signer Signer, tx *Transaction) (common.Address, error) {
    if sc := tx.from().Load(); sc != null {
        sigCache := sc.(sigCache)// cache exists,
        if sigCache.signer.Equal(signer) {
            return sigCache.from, nil
        } 
    }
    addr, err := signer.Sender(tx)
    if err != nil {
        return common.Address{}, err
    }
    tx.from.Store(sigCache{signer: signer, from: addr}) // cache it
    return addr, nil
}

Sender()函数体中,signer.Sender()会从本次数字签名的签名字符串(signture)中恢复公钥,并转化为转出方地址。
在上文提到的ApplyTransaction()实现中,Transaction对象需要首先被转化成Message接口,用到的AsMessage()函数即调用了此处的Sender()。

// core/types/transaction.go
func (tx *Transaction) AsMessage(s Signer) (Message,error) {
    msg := Message{
        price: new(big.Int).Set(tx.data.price)
        gasLimit: new(big.Int).Set(tx.data.GasLimit)
        ...
    }
    var err error
    msg.from, err = Sender(s, tx)
    return msg, err
}

在Transaction对象tx的转帐转出方地址被解析出以后,tx 就被完全转换成了Message类型,可以提供给虚拟机EVM执行了。
11、虚拟机内部每个交易带有两部分内容需要执行:1、转账,由转出方地址向转入方地址转账一笔以太币Ether。2、携带的[]byte类型成员变量Payload,其每一个byte都对应了一个单独的虚拟机指令。这些都是由EVM对象来完成的。EVM结构体是Ethereum虚拟机机制的核心。它与协同类NML关系图如下:
在这里插入图片描述
其中Context结构体带有Transaction的信息(GasPrice和GasLimit),Block的信息(Number,Difficulty),以及转账函数等,提供给EVM。StateDB 接口是针对state.StateDB 结构体设计的本地行为接口,可为EVM提供statedb的相关操作; Interpreter结构体作为解释器,用来解释执行EVM中合约(Contract)的指令(Code)。
注意,EVM 中定义的成员变量Context和StateDB, 仅仅声明了变量名而无类型,而变量名同时又是其类型名,在Golang中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如EVM调用Context中的Transfer()。
12、交易转账工作由Context对象中的TransferFunc类函数来实现,类似的函数类型,还有CanTransferFunc, 和GetHashFunc。

// core/vm/evm.go
type {
    CanTransferFunc func(StateDB, common.Address, *big.Int)
    TransferFunc func(StateDB, common.Address, common.Address, *big.Int)
    GetHashFunc func(uint64) common.Hash
} 
// core/evm.go
func NewEVMContext(msg Message, header *Header, chain ChainContext, author *Address){
    return vm.Context {
        CanTransfer: CanTransfer,
        Transfer: Transfer,
        GetHash: GetHash(header, chain),
        ...
    }
}
 
func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) {
    return db.GetBalance(addr).Cmp(amount) >= 0
}
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
    db.SubBalance(sender, amount)
    db.AddBalance(recipient, amount)
}

转账函数Transfer()的逻辑是转账的转出账户减掉一笔以太币,转入账户加上一笔以太币。StateDB 并不是真正的数据库,只是一行为类似数据库的结构体。它在内部以Trie的数据结构来管理各个基于地址的账户,可以理解成一个cache;当该账户的信息有变化时,变化先存储在Trie中。仅当整个Block要被插入到BlockChain时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。所以这里Transfer()函数中的转出和转入账户不会立即生效。
12、合约(Contract)是EVM用来执行(虚拟机)指令的结构体。

// core/vm/contract.go
type ContractRef interface {
    Address() common.Address
}
type Contract struct {
    CallerAddress common.Address
    caller ContractRef
    self ContractRef
 
    jumpdests destinations
    Code []byte
    CodeHash common.Hash
    CodeAddr *Address
    Input []byte
    Gas uint64
    value *big.Int
    Args []byte
    DelegateCall bool
}

1)、caller是转帐转出方地址(账户),self是转入方地址,不过它们的类型都用接口ContractRef来表示;Code是指令数组,其中每一个byte都对应于一个预定义的虚拟机指令;CodeHash 是Code的RLP哈希值;Input是数据数组,是指令所操作的数据集合;Args 是参数。
2)、EVM目前有五个函数可以创建并执行Contract,并按照作用和调用方式,可以分为两类:
Create(), Call(): 二者均在StateProcessor的ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。
CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐。
3)其中Call()用来处理转账转入方地址不为空的情况。
在这里插入图片描述
Call()函数的逻辑可以简单分为以上6步。其中步骤(3)调用了转帐函数Transfer(),转入账户caller, 转出账户addr;步骤(4)创建一个Contract对象,并初始化其成员变量caller, self(addr), value和gas; 步骤(5)赋值Contract对象的Code, CodeHash, CodeAddr成员变量;步骤(6) 调用run()函数执行该合约的指令,最后Call()函数返回。相关代码可见:

// core/vm/evm.go
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftGas *big.Int, error){
    ...
    var snapshot = evm.StateDB.Snapshot()
    contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
    ret, err = run(evm, snapshot, contract, input)
    return ret, contract.Gas, err
}

因为此时(转帐)转入地址不为空,所以直接将入参addr初始化Contract对象的self地址,并可从StateDB中(其实是以addr标识的账户stateObject对象)读取出相关的Code和CodeHash并赋值给contract的成员变量。注意,此时转入方地址参数addr同时亦被赋值予contract.CodeAddr。
4)EVM.Create(),它用来处理转账转入方地址为空的情况。
在这里插入图片描述
与Call()相比,Create()因为没有Address类型的入参addr,其流程有几处明显不同:

步骤(3)中创建一个新地址contractAddr,作为(转帐)转入方地址,亦作为Contract的self地址;
步骤(6)由于contracrAddr刚刚新建,db中尚无与该地址相关的Code信息,所以会将类型为[]byte的入参code,赋值予Contract对象的Code成员;
步骤(8)将本次执行合约的返回结果,作为contractAddr所对应账户(stateObject对象)的Code储存起来,以备下次调用。
还有一点隐藏的比较深,Call()有一个入参input类型为[]byte,而Create()有一个入参code类型同样为[]byte,没有入参input,它们之间有无关系?其实,它们来源都是Transaction对象tx的成员变量Payload!调用EVM.Create()或Call()的入口在StateTransition.TransitionDb()中,当tx.Recipent为空时,tx.data.Payload 被当作所创建Contract的Code;当tx.Recipient 不为空时,tx.data.Payload 被当作Contract的Input。
13、EVM中执行合约的函数时run(),其代码如下:

// core/vm/evm.go
func run(evm *EVM, snapshot int, contract *Contract, input []byte) ([]byte, error) {
    if contract.CodeAddr != nil {
        precompiles := PrecompiledContractsHomestead
        ...
        if p := precompiles[*contract.CodeAddr]; p != nil {
            return RunPrecompiledContract(p, input, contract)
        }
    }
    return evm.interpreter.Run(snapshot, contract, input)
}

可见如果待执行的Contract对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr为匹配项-那么它可以直接运行;没有经过预编译的Contract,才会由Interpreter解释执行。这里的"预编译",可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要Code,仅需Input即可。
在代码实现中,预编译合约只需实现两个方法Required()和Run()即可,这两方法仅需一个入参input。

// core/vm/contracts.go
type PrecompiledContract interface {
    RequiredGas(input []byte) uint64
    Run(input []byte) ([]byte, error)
}
func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {
    gas := p.RequiredGas(input)
    if contract.UseGas(gas) {
        return p.Run(input)
    }
    return nil, ErrOutOfGas
}

13、解释器Interpreter用来执行(非预编译的)合约指令。它的结构体UML关系图如下所示:
在这里插入图片描述
Interpreter结构体通过一个Config类型的成员变量,间接持有一个包括256个operation对象在内的数组JumpTable。operation是做什么的呢?每个operation对象正对应一个已定义的虚拟机指令,它所含有的四个函数变量execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。每个指令长度1byte,Contract对象的成员变量Code类型为[]byte,就是这些虚拟机指令的任意集合。operation对象的函数操作,主要会用到Stack,Memory, IntPool 这几个自定义的数据结构。

Interpreter的Run()函数就很好理解了,其核心流程就是逐个byte遍历入参Contract对象的Code变量,将其解释为一个已知的operation,然后依次调用该operation对象的四个函数,流程示意图如下:
在这里插入图片描述
operation在操作过程中,会需要几个数据结构: Stack,实现了标准容器 -栈的行为;Memory,一个字节数组,可表示线性排列的任意数据;还有一个intPool,提供对big.Int数据的存储和读取。
需要特别注意的是LOGn指令操作,它用来创建n个Log对象,这里n最大是4。还记得Log在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个Receipt对象用来记录这个交易的执行结果。Receipt携带一个Log数组,用来记录tx操作过程中的所有变动细节,而这些Log,正是通过合适的LOGn指令-即合约指令数组(Contract.Code)中的单个byte,在其对应的operation里被创建出来的。每个新创建的Log对象被缓存在StateDB中的相对应的stateObject里,待需要时从StateDB中读取。
14、以太坊的出现大大晚于比特币,虽然明显受到比特币系统的启发,但在整个功能定位和设计架构上却做了很多更广更深的思考和尝试。以太坊更像是一个经济活动平台,而并不局限一种去中心化数字代币的产生,分发和流转。本文从交易执行的角度切入以太坊的系统实现,希望能提供一点管中窥豹的作用。

Gas是Ethereum系统的血液。一切资源,活动,交互的开销,都以Gas为计量单元。如果定义了一个GasPrice,那么所有的Gas消耗亦可等价于以太币Ether。
Block是Transaction的集合。Block在插入BlockChain前,需要将所有Transaction逐个执行。Transaction的执行会消耗发起方的Ether,但系统在其执行完成时,会给予其作者(挖掘出这个Block的账户)一笔补偿,这笔补偿是“矿工”赚取收入的来源之一。
Ethereum 定义了自己的虚拟机EVM, 它与合约(Contract)机制相结合,能够在提供非常丰富的操作的同时,又能很好的控制存储空间和运行速度。Contract由Transaction转化得到。
Ethereum 里的哈希函数,用的是SHA-3,256 bits;数据(数组)的序列化,用的是RLP编码,所以所有对象,数组的哈希算法,实际用的RLP + SHA-3。数字签名算法,使用了椭圆曲线数字签名算法(ECDSA)。

发布了29 篇原创文章 · 获赞 5 · 访问量 4611

猜你喜欢

转载自blog.csdn.net/guoyihaoguoyihao/article/details/100597794