Comprehensive Health Chain

TECHNOIOGY  技术
UTXO模型剖析
Account模型剖析
交易和虚拟机
共识机制

UTXO模型剖析

        UTXO的英文全称为Unspent Transaction Output,即未消费的交易输出。我们可以把UTXO理解为交易过程中的一个数据结构。未花费的交易输出UTXO是一个包含交易数据和执行代码的数据结构,可以通俗的理解为某仓库(某地址)已经收到的但是尚未花费出去的加密数字货币。基于区块链的加密数字货币使用UTXO来验证一个人(其实是一个地址)是否拥有未使用过的加密数字货币用于支付。UTXO可以看做被私钥的拥有者锁定的、并被整个比特币网络识别的比特币货币单位。

UTXO模型中,被某一个交易消耗的UTXO被称为交易输入,由交易创建的UTXO被称为交易输出。通过这种方式,一定量的比特币在不同的私钥所有者之间转移,并在交易链条中不断消耗和创建新的UTXO。一笔比特币交易通过所有者的私钥签名来解锁UTXO,并通过使用新的所有者的比特币地址来锁定并创建UTXO。在比特币网络的起始的阶段,矿工通过一种特殊的交易类型,Coinbase交易创造的交易的输出(该交易没有输入),所产生的比特币,可以用于创建其他的UTXO

UTXO被每一个全节点(FullNode)比特币客户端在一个储存于内存中的数据库所追踪,该数据库也被称为“UTXO或者“UTXO,新的交易构建时从UTXO池中消耗一个或多个输出,而比特币网络监测着以百万为单位的所有可用的UTXO,世界上在比特币网络中并不存在比特币余额的概念,因为比特币网络上只会记录所有未花费的UTXO,比特币的余额的概念更多是通过比特币钱包客户端派生出来的产物,比特币钱包通过扫区块链并聚合所有属于该用户的UTXO来计算该用户的余额。

比特币网络中的每一笔交易的执行依赖于解锁脚本和锁定脚本。解锁脚本可以解决锁定脚本对某一输出值的阻碍,锁定脚本会在某一笔输出值上设置花费的条件。解锁脚本通常包含私钥的一个签名,也被称为ScriptSig,锁定脚本通常会把一个交易输出锁定到一个比特币地址上(公钥的哈希Hash)

比特币全节点客户端会同时执行锁定脚本和解锁脚本来验证某一笔交易的合法性。客户端会先检索输入所指向的UTXO,这个UTXO包含一个定义了花费条件的锁定脚本,然后客户端会读取试图花费这个UTXO的由客户端构造的输入中所包含的解锁脚本,并执行这两个脚本。如果从解锁脚本处复制好堆栈数据之后,再执行锁定脚本的结果为真,那么说明解锁脚本有权使用该UTXO,并发起新的交易。

比特币解锁脚本和锁定脚本示意图

        与UTXO模型不同的是,以太坊是有账户体系的,在以太坊的白皮书中,我们可以看到以太坊的账户体系:

在以太坊系统中,状态是由被称为“账户”(每个账户由一个20字节的地址)的对象和在两个账户之间转移价值和信息的状态转换构成的。以太坊的账户包含四个部分:

  随机数,用于确定每笔交易只能被处理一次的计数器

  账户目前的以太币余额

  账户的合约代码,如果有的话

  账户的存储(默认为空)

        以太币(Ether)是以太坊内部的主要加密燃料,用于支付交易费用。一般而言,以太坊有两种类型的账户:外部所有的账户(由私钥控制的)和合约账户(由合约代码控制)。外部所有的账户没有代码,人们可以通过创建和签名一笔交易从一个外部账户发送消息。每当合约账户收到一条消息,合约内部的代码就会被激活,允许它对内部存储进行读取和写入,和发送其它消息或者创建合约。

        在以太坊系统中,通过一个有状态的账户系统来记录账户余额,每个账户余额的增加/减少更像现实世界中的银行记账方式,每产生一个新的区块,都会可能对全局状态造成影响。每个账户都有自己的余额、存储和代码区域。这样合约就可以调用账户或者地址,并且把相应的执行结果在存储区域进行存储。

        在目前以太坊的账户系统中,通过client/rpc,只能进行一对一的转账,也就意味着每次只能从一个账户转移到另外一个账户。尽管通过智能合约可以发送到更多的账户,但是这些内部交易只能在用户的账户余额上显示,却很难在以太坊的公开账本上追踪。

        在最近的以太坊实现中,我们有了一个显式的协议层概念,关于账户和交易中的连续数字;因此,我们已经为我们的用户做出了抉择,这是我们用来保护账户的模式。一个抽象模式将这一选择从协议层下沉到 EVM;本质上,每一个用户都将可以为他们自己选择用于保护他们账户的机制。这打开了朝向创造性的大门,比如,K-可并行化nonce(本质上,这个方案结合了一个带有千位二进制过滤器的nonce,保证nonce是一次性使用的,但允许用户提前使用未来的nonce,允许高达K笔交易以任意顺序处理),甚至允许用户建立基于 UTXO 的方案,如果他们希望的话。

以太坊网络的ACCOUNT模型,保存了交易的状态数据可追溯,也是以太坊架构的核心设计,考虑到以太坊的网络效应和ACCOUNT模型的优点,在CHC的公链系统中,我们第一步决定采取基于ACCOUNT模型。


Account模型剖析


POW:工作量证明(Proof Of Work)
     工作量证明是矿工在处理交易数据(对数据也是进行哈希)的同时不断的进行哈希计算,求得一位前23位为0的哈希值,这个值成为nonce黄金数。当全网有一位矿工哈希出nonce时,他就会把自己打包的区块公布出去,其他节点收到区块验证区块后就会一致性认为这个区块接到了区块链上,就继续进行下一个区块的打包和哈希计算。在这个过程中,中本聪大神是通过算力的比拼牺牲了一部分最终一致性(因为会有分叉的产生)并且需要等待多个确认,但是这种简单暴力的方法却保证了整个区块链系统的合法性,而且把区块链系统的健壮性提升到极致,就算全网只剩下一个节点运行,这个区块链系统还是会继续运行下去。最后POW也充分提高了区块链系统的安全性,依靠51%攻击理论去破坏区块链系统是只有政府或者疯子才会采取的方法。
    优点:完全去中心化;节点自由进出,容易实现;破坏系统花费的成本巨大。
    缺点:对节点的性能网络环境要求高;无法达成最终一致性;浪费能源。
POS:权益证明
        POS是根据钱包里面货币的多少以及货币在钱包里存在的天数来合成一个单位(币天)。它根据币天的关系对计算机进行哈希计算降低了难度,降低了计算机的门槛,但是对计算机还是有一定要求的,它把钱包和区块链系统的一致性绑定在一起。谁的钱包里的币天数越大谁拥有记账权的概率就越大。但是它和POW机制一样解决问题的思想也导致了它与POW拥有一样的缺点,也是牺牲了一部分的共识(同样分叉),而且需要等待多个确认。
优点:对节点性能要求低,达成共识时间短;
缺点:没有最终一致性。
DPOS:股份授权证明机制
是基于POS衍生出的更专业的解决方案,他是类似于董事会的投票机制,选举出n个记账节点,在节点中提案者提交的提案被这些记账节点投票决定谁是正确的。
优点:减少记账节点规模,属于弱中心化,效率提高;
缺点:牺牲了去中心化的概念,不适合公有链
在CHC公链系统中,选取Proof of Stake的一些权衡:去中心化的程度、节点参与记账的难度、网络的维护成本。


狭义的交易可能仅仅是一笔转帐,而广义的交易同时还会支持许多其他的意图。CHC中采用的是广义交易概念,交易的执行可大致分为内外两层结构:第一层是虚拟机外,包括执行前将Transaction类型转化成Message,创建虚拟机(EVM)对象,计算一些Gas消耗,以及执行交易完毕后创建收据(Receipt)对象并返回等;第二层是虚拟机内,包括执行转帐,和创建合约并执行合约的指令数组。

执行tx的入口函数是StateProcessorProcess()函数,其实现代码如下:

# /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  

}  

GasPool 类型其实就是big.Int。在一个Block的处理过程(即其所有tx的执行过程)中,GasPool 的值能够告诉你,剩下还有多少Gas可以使用。在每一个tx执行过程中,Ethereum 还设计了偿退(refund)环节,所偿退的Gas数量也会加到这个GasPool里。

Process()函数的核心是一个for循环,它将Block里的所有tx逐个遍历执行。具体的执行函数叫ApplyTransaction(),它每次执行tx, 会返回一个收据(Receipt)对象。Receipt结构体的声明如下:


Receipt 中有一个Log类型的数组,其中每一个Log对象记录了Tx中一小步的操作。所以,每一个tx的执行结果,由一个Receipt对象来表示;更详细的内容,由一组Log对象来记录。这个Log数组很重要,比如在不同Ethereum节点(Node)的相互同步过程中,待同步区块的Log数组有助于验证同步中收到的block是否正确和完整,所以会被单独同步(传输)

ReceiptPostState保存了创建该Receipt对象时,整个Block内所有帐户的当时状态。Ethereum 里用stateObject来表示一个账户Account,这个账户可转帐(transfer value), 可执行tx, 它的唯一标示符是一个Address类型变量。 这个Receipt.PostState 就是当时所在Block里所有stateObject对象的RLP Hash值。

Bloom类型是一个Ethereum内部实现的一个256bitBloom Filter Bloom Filter概念定义可见wikipedia,它可用来快速验证一个新收到的对象是否处于一个已知的大量对象集合之中。这里ReceiptBloom,被用以验证某个给定的Log是否处于Receipt已有的Log数组中。

每个交易(Transaction)带有两部分内容需要执行:1. 转帐,由转出方地址向转入方地址转帐一笔以太币Ether; 2. 携带的[]byte类型成员变量Payload,其每一个byte都对应了一个单独虚拟机指令。这些内容都是由EVM(Ethereum Virtual Machine)对象来完成的。

EVM 结构体是Ethereum虚拟机机制的核心,它与协同类的UML关系图如下:

Context结构体分别携带了Transaction的信息(GasPrice, GasLimit)Block的信息(Number, Difficulty),以及转帐函数等,提供给EVMStateDB 接口是针对state.StateDB 结构体设计的本地行为接口,可为EVM提供statedb的相关操作; Interpreter结构体作为解释器,用来解释执行EVM中合约(Contract)的指令(Code)

注意,EVM 中定义的成员变量ContextStateDB, 仅仅声明了变量名而无类型,而变量名同时又是类型名,在Golang中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如EVM调用Context中的Transfer

交易的转帐操作由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  

}   

这三个类型的函数变量CanTransfer, Transfer, GetHash,在Context初始化时从外部传入,目前使用的均是一个本地实现:

// 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)  

}