区块链 2.0
以太坊概述
相对于比特币的几点改进
- 缩短出块时间至10多秒
- ghost共识机制
- mining puzzle
- BTC:计算密集型
- ETH:memory-hard(限制ASIC)
- proof of work->proof of stake
- 对智能合约的支持
- BTC:decentralized currency
- ETH:decentralized contract(去中心化合约) 优势
以太坊账户
比特币是基于交易的账本模式(transaction-based ledger),每笔交易都是由输入和输出构成的。输入引用先前交易的输出,以证明发送者有权使用这些比特币。而输出则指定接收方地址和转移给接收方的比特币数量。因此要想知道余额,要去UTXO找关联账户
举个例子:
假设Alice拥有10个比特币,她想将其中3个比特币发送给Bob。这将产生一笔交易,输入是10个比特币(来自Alice之前的交易输出),输出是3个比特币(给Bob)和7个比特币(返回给Alice自己,因为剩下的比特币必须归还给发送方)。
当这笔交易被确认后,系统中不再存在先前的10个比特币的“账户余额”概念。相反,账本中将存在两个未使用交易输出:一个是给Bob的3个比特币,另一个是给Alice的7个比特币。这些未使用的交易输出可以用于构建下一笔交易。
以太坊系统是基于账户的模型(account-based ledger)
在基于账户的账本模式中,区块链记录着各个账户及其相应的余额。每个账户都有一个地址(类似于银行账户号码)和与之相关的以太币余额。当以太坊网络上发生交易时,涉及的是在不同账户之间转移以太币。
假设账户B想要向账户C发送以太币。一笔交易被创建,指定了发送方(账户B)、接收方(账户C)以及要转移的以太币数量。交易被确认并添加到以太坊区块链的一个区块后,账户余额相应地进行更新。以太币从账户B的余额中减少,同时同样的数量被增加到账户C的余额中。
account based模式的优点
- 符合人的主观感受和现行银行交易类似
- 可以防范double speeding attack
- 高可扩展性:基于账户的模式在处理大量交易时有较好的扩展性。由于不需要跟踪每个交易的UTXO,交易处理速度相对较快,从而提高了整个网络的吞吐量。
account based模式的缺点
1.replay attack(nonce计数器)
- 在账户模式下,需要存储每个账户的余额和状态信息。随着账户数量和交易量的增加,需要更大的存储空间来保存所有账户的信息,这可能会导致存储成本的增加
账户类别
外部账户(externally owned account)
- balance
- nonce
外部账户也被称为EOA账户或用户账户。它们是由用户生成和控制的标准账户,通过私钥和公钥来实现身份认证和交易签名。
- 每个外部账户有一个对应的以太币(ETH)余额,可以发送和接收以太币。
- 外部账户可以用于参与普通的货币交易,例如转账以太币给其他账户。
- 外部账户通过私钥签署交易,然后将交易广播到区块链网络。
Smart contract account(合约账户)
- balance
- nonce(一个合约可以调用另外一个合约,因此通过nonce记录调用的次数)
- code
- storge(调用过程会改变)
合约账户是一种由智能合约代码控制的特殊账户,它们不依赖于私钥和公钥进行身份认证。
- 智能合约是一种以编程方式定义的自动执行的计算机代码,能够根据预设规则和条件执行交易或实现特定功能。
- 智能合约代码被部署到以太坊上,生成一个合约账户,并分配一个以太币余额用于支付交易执行的成本(称为“燃气”费用)。
- 合约账户可以处理复杂的业务逻辑和多方交互,可以实现更为复杂的去中心化应用(DApps)。
智能合约要求有稳定的账户参与
以太坊中的状态树
思路一:使用hash表查询
存在问题:每次出现新交易打包进区块中,从而改变Merkle tree,但事实上只有一部分发生改变,一部分改变要重写整个Merkle tree
(另外由于BTC)
思路二:直接使用Merkle tree存取账户,要改直接改merkle tree,是否可行?
- merkle tree查找效率低
- 不排序,存在的问题:当计算叶节点顺序不能达成一致(BTC的顺序是由拥有铸币权的节点决定的)
如果使用sorted merkle tree呢?
插入账户时,merkle tree的重构代价很大
MTP结构
Trie结构(Trie是一种用于存储和快速检索键值对的树状数据结构,通常用于高效地存储大量的关联数据。)可以类比信号的状态转移
trie结构的特点:
- 打乱顺序,trie结构不变
- 具有很好的更新操作局部性
trie缺点:存储浪费,部分内容效率低(键值的分布稀疏)
Patricia Tree(压缩前缀树)经过了路径压缩的树
树的高度明显减少(如果树的分布很稀疏,较为明显)
disintermediarion(去中介)
为了防止哈希碰撞,所以地址设置为 2 160 2^{160} 2160这么大
Merkle Patricia Tree
把普通指针换成了哈希指针
根节点(Root Node):状态树的根节点是一个哈希值,用于表示整个状态树的当前状态。所有账户状态数据都通过这个根节点的哈希值进行唯一标识。
分支节点(Branch Node):分支节点是状态树中的中间节点,用于连接不同的叶子节点或其他分支节点。每个分支节点有17个子节点,对应了16进制数字0-15和一个特殊节点(NIL节点)。每个子节点保存一个哈希值,指向下一级的分支节点或叶子节点。
叶子节点(Leaf Node):叶子节点包含了账户的具体状态信息。每个叶子节点对应一个账户地址,保存了与该地址相关的账户状态数据,如余额、代码、存储等。
这种状态树的结构使得以太坊能够高效地跟踪和维护账户的状态,同时也有助于实现轻客户端验证和状态证明等功能,从而提高整个网络的可扩展性和安全性。
以太坊实际用的结构是modified MPT
根节点哈希值存在块头
每次发布区块时,状态树中新节点的值会发生改变,这些改变是局部的
共享分支
问题:为什么保留历史状态,不在原先数据上进行修改?
答:为了回滚,在ETH中分叉是常态,orphan block中的数据都要向前一状态回滚,而由于ETH中有智能合约,为了支持智能合约的回滚,必须保持之前的状态
区块头结构
发布的信息
状态树保存的是(key,value),上面讲的都是key保存的地址,那么Value呢?
经过RLP(Recursive Length Prefix)序列化再存储
只做nested array of bytes
区块链的交易树和收据树
状态树:是包含多个区块的状态
交易树和收据树(独立的区块的交易和交易结果)的作用:
-
提供merkle proof
-
支持更复杂的查询操作->e.g.支持查找过去10天和某个智能合约相关的交易
解决方案:引入了bloom filter数据结构(有可能出现false postive(误报),而不会出现false passive(漏报))
bloom filter数据结构
布隆过滤器(Bloom Filter)是一种概率型数据结构,用于快速判断一个元素是否属于某个集合。它可以高效地检索元素,同时占用较少的内存空间。布隆过滤器的主要应用场景是在大规模数据集中进行快速查找和过滤操作。
ETH有关bloom filter的具体操作
每个交易执行完后会形成一个收据,收据中包含一个bloom filter记录这个交易的类型,地址等其他信息
发布的区块在它的块头,也有一个总的blooom filter,这个总的blooom filter,是该区块中所有交易的bloom filter的并集
当我们需要查找过去10天的有关智能合约的所有交易的方法是:
step1:找哪个区块的块头中的bloom filter有我们需要的交易的类型,如果块头中没有,那么这个区块不是我们所需要的区块
step2:如果块头有,就去这个区块查找各个收据的bloom filter,如果有则去确定交易
ETH运行过程:交易驱动的状态机(tx-driven state machine)
Q:现行状态树的机制中,包含全部的账户的信息的状态,可否改成只包含区块中涉及的tx的账户的状态?
- 每个节点所涉及的交易可能会不完整,查找某账号状态不方便
当前状态树的设计保证了每个区块的状态是完整和一致的。如果只包含区块中涉及的交易的账户状态,那么可能会出现信息丢失或不一致的情况,从而导致网络的不安全性。
- 新建账户的查找麻烦,最差得找到genisis block才能发现是新建账户
GHOST协议
分叉在以太坊是常态
ETH将区块生成时间设置为10秒可能带来的危害
但对ETH来说,出块时间太短,如果继续使用最长合法链,对于个人矿工尤其不公平,因为大矿场有能力去制造最长合法链
mining pool
GHOST协议核心:给挖到orphan blocks的节点“安慰奖”
考虑分叉区块:在GHOST协议中,当节点在选择最长链时,不仅考虑最长链上的区块,还会考虑其他分叉区块(即不在最长链上的区块)。具体来说,节点会选择最重的分支链,而不仅仅是最长链。
奖励叔块:在GHOST协议中,叔块(Uncle Block,也称叔节点)是指那些没有被选择为最终区块的分叉区块(最多包含两个Uncle Block)。虽然叔块没有包含在最长链中,但它们也对区块链的安全性和分叉问题有积极的贡献。GHOST协议会对叔块的挖矿者给予一定的奖励,以激励挖矿者选择更广泛的区块,增加网络的整体安全性。
Uncle Block可以一直延续下去,最多能够延续7代奖励
state fork
问题:把uncle block 的tx包括进来,其中的tx是否执行?
答:不执行,交易可能有重复,并且也不检查uncle block的交易的合法性
问题:uncle block 后面还跟着一串怎么办?是否算叔父区块?
ETH实际
可参考 etherscan网站上的tx
以太坊的挖矿算法
Ethash
Block chain is secured by mining
BTC mining饱受争议的地方在于:要用专业的ASIC芯片,这与去中心化理念背道而驰
ASIC resistence一个重要的方法是memory hard mining puzzle
例子:Litecoin(基于Scrypt)
ETH的改进,2种数据集
Ethash是一种重量级的PoW算法,许多最流行的加密货币都加以使用,包括以太坊,以及墨客(MOAC)、Expanse、Pirl等。该算法是不同的,因为它使用的DAG文件,是在矿工启动的那一刻被加载到GPU内存。每30000个区块就会发生一次纪元变化,使得DAG文件增加8MB。
ETHASH改良了Dagger-Hashimoto,有效地解决了单纯内存依赖的算法诸如Scrypt算法加密难与解密同样难的困境,也突破Dagger算法不抵抗内存共享硬件加速的困境,从全区块链数据的生成改为固定的1GB的数据的生成,支持了客户端预生成数据,保障挖矿难度的平滑过度。
16M cache -> 轻节点,便于验证
1G dataset -> 全节点
具体过程
16M cache生成方式与Scrypt类似
step0 计算一个种子(Seed),该种子的计算依赖于当本块及本块之前的所有块。
step1 在数组中首位填入随机数(nonce),之后的数用hash函数算出来,并依次填充cache中的元素(256 bits)
从种子中计算得出一个缓存(Cache),该缓存仅仅由种子得出,是一个16MB大小的数据集。轻客户端应存储下该缓存用于日后验证。
step2 得到256个的hash放入更大的数组当中(1G dataset)
step3 找nonce,通过nonce读取对应位置(以及循环),往复64次,找到128个数,取hash,去解puzzle
挖矿过程即为哈希过程。哈希的输入是取得1GB数据集的128个子部分,并将它们放在一起执行哈希。验证过程是可以在轻客户端通过Cache缓存生成被挖矿制定的数据碎片并执行哈希验证。故而轻客户端并不需要时刻保存1GB的DAG。
伪代码分析
step 1
#step1 生成16Mcache
def mkcache(cache_size,seed):o=[hash(seed)]for i in range(cache_size):o.append(hash(o[-1]))#hash函数 o[-1]是上一个hash值return o
step 2
#step2 通过cache生成dataset中的第i个数据
def calc_dataset_item(cache,i):cache_size=cache.size#cache的大小mix=hash(cache[i%cache_size]^i)#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)
step 3多次调用这个函数,得到完整的dataset
puzzle算法实现
为什么矿工要保存所有的dataset,而情节点只需要保存cache?
以太坊总供应量
https://etherscan.io/stat/supply
以太坊挖矿难度调整
ETH的挖矿难度调整操作很多有出入,因此这里以官方代码为主
PART 1:主公式
PART 2
参数说明: x x x是调整的单位, ζ 2 \zeta_{2} ζ2为调整的系数
H s H_s Hs是当前的时间戳, P ( H ) H S P(H)_{H_S} P(H)HS是父区块的时间戳,两者时间差为出块间隔
PART 3
参数说明
权益证明
POW饱受批评的一点在于浪费资源
问题:挖矿消耗的能耗是否是必须得
挖矿的本质:大家比拼算力,算力越大,挖矿机率越高,因此本质为:投入的钱越多,挖矿机率越高
POS核心思想:直接拼钱->virtual mining
POS vs. POW优点:
- 节能
- POS是闭环生态,而POW是开放生态,因此POS天然防止51% attack(attacker必须购买足够多的加密货币(成为股东或者获取更多的投票权),才有发动attack的能力,但此时,对于币的开发者和早期矿工是受益的,如同恶意收购)
noting at state:当出现下述分叉时,pow会选择一条链,而POS下两条链都可以下注(且没有损失)
ETH准备采用的POS协议是Casper the Friendly Finality Gadget(FFG)
引入了:验证者validator->投入一定数量的保证金
每挖出100个区块生成一个epoch,然后开始投票(two-phase commit)
投票中需要有两轮投票(都要达到 2 3 \frac{2}{3} 32)
每个epoch只投票一次(validator投票)
对于validator来说,积极参与投票有奖励,行政不作为则会受到处罚(扣保证金)
而对于两边下注的行为,没收全部保证金
以太坊虚拟机
在以太坊上最重要的活动除了转账以外,就是编译、运行智能合约(Smart Contract)。
智能合约代表了一个以太坊世界里的独立管家。它按照自身代码指示进行以太币的收入,支出活动,也具有一定存储空间可以存储一些数据。
以太坊虚拟机执行分为两大类,只读操作和写操作。 仅获取区块链状态的操作为只读操作。
只读操作并不修改区块链状态,在链式调用合约的时候也不会触发任何状态变更,所以较为迅速。
写操作则会改变区块链的状态。例如一个更改账户状态的操作。
写操作则是需要花费以太币的操作,因为它更改了某一个或者数个账户的存储空间。 存储空间是区块链的一部分,是要被全世界的计算机永久同步存储的,这个更改的过程代价昂贵: 例如将一个值从 0
变为非零值需要耗费 20000
单位的gas;修改一个 非0
值需要消耗 5000
单位gas; 将一个值从 非0
赋值为 0
可以回收 15000
单位的gas;读取一个变量值需要 200
gas。而相对比,从内存中读取变量值仅需 3
gas。
以太坊的虚拟机不包含正常CPU所具备的硬件寄存器, 它的执行宽度都是 256bit 的固定长度值,所以相对比较容易编程,它包含了存储、堆栈、内存三大存储机构。
Storage存储
存储里的值都是永久记录在区块链上的。存储在写和读取上都代价昂贵,如非必须,则数据不要存储在存储区。
存储区的读写操作都是以 256bit 为单位的,没有更小的操作空间。故而 uint8 和 uint256 在单值存储的情况下占用的空间相同,耗费的 Gas 也相同。 将 unit8 包裹进入 struct 结构体以后可以通过优化来节约空间位置,节省gas支出。
Stack堆栈
堆栈有且仅有 1024 层深度,当我们执行递归调用过多的时候,堆栈就会击穿 1024 层,则代码执行失败。
堆栈仅有高处的 16 层是可以被快速访问的,堆栈的宽度也是 256bit,也就是 32byte,一个 word 的长度,任何读写操作都是 256bit 为一个单位进行的。 编译器往往会将代码执行中的临时变量、变量地址放到堆栈上临时保存,变量地址可以进一步索引到内存 Memory 中。
Memory 内存
虚拟机的输入输出
Gas花费
发送交易的时候我们可以指定 gas 的花费上限,以防止智能合约代码有bug而导致无限循环执行下去。
一旦 gas 过早耗尽,则虚拟机抛出异常,结束代码执行。
有一类情况很特殊,就是Solidity智能合约代码的 assert()
函数与 require()
函数。
在执行时候这两个函数都是做真假条件判断的,但是 assert 函数感情更加强烈,往往判断的都是关键的安全性条件,例如 SafeMath 中利用 assert 函数判断是否位数溢出; 合约取款时判断调用方是否为合约所有者等。
require()函数则较为普通的条件判断,例如判断合约调用者的余额是否足够等。
assert() 函数判断一旦失败,则会扣完剩下所有的 gas 作为惩罚措施; require() 判断失败则仅仅停止目前的执行,收取执行到当前步骤相应的 gas 费用,再撤销发生的变更。
虚拟机指令集
EVM执行的是字节码。由于操作码被限制在一个字节以内,所以EVM指令集最多只能容纳256条指令。
mem[a…b] 表示内存中a到b(不包含b)个字节
storage[p] 表示从p开始的32个字节
谨记evm虚拟机的word(字)是256位32字节
智能合约
什么是智能合约?
智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容
智能合约的帐户保存了合约当前的运行状态
- balance:当前余额
- nonce:交易次数
- code:合约代码
- storage:存储,数据结构是一棵MPT
- Solidity是智能合约最常用的语言,语法上与JavaScript很接近
以太坊中凡是需要接受外部转账的函数都需要标记为payable
solidity语言不支持hash表的遍历
bidders.push(bidder)//添加竞拍人
bidders.length//大小
调用智能合约
创建一个交易,接收地址为要调用的那个智能合约的地址,data域填写要调用的函数及其参数的编码值。
1.直接调用
3.代理调用
代理调用delegatecall()
使用方法与call()相同,只是不能使用.value()
区别在于是否切换上下文
- call0切换到被调用的智能合约上下文中
- delegatecall()只使用给定地址的代码,其它属性(存储,余额等)都取自当前合约。delegatecall的目的是使用存储在另外一个合约中的库代码。
fallback()函数
function()public [payable]{
......
}
- 匿名函数,没有参数也没有返回值。
- 在两种情况下会被调用:
- 直接向一个合约地址转账而不加任何data
- 被调用的函数不存在
- 如果转账金额不是0,同样需要声明payable,否则会抛出异常。
智能合约的创建和运行
- 智能合约的代码写完后,要编译成bytecode
- 创建合约:外部帐户发起一个转账交易到0x0的地址
- 转账的金额是0,但是要支付汽油费
- 合约的代码放在data域里
- 智能合约运行在EVM(Ethereum Virtual Machine)上
以太坊是一个交易驱动的状态机- 调用智能合约的交易发布到区块链上后,每个矿工都会执行这
个交易,从当前状态确定性地转移到下一个状态
- 调用智能合约的交易发布到区块链上后,每个矿工都会执行这
Gas fee
ETH的错误处理
ETH交易具有原子性,即一个交易要么不执行,要么全执行,不会执行一部分
-
智能合约不存在自定义的try-catch结构
-
一旦遇到异常,除特殊情况外,本次执行操作全部回滚
-
可以抛出错误的语句
-
assert(bool condition)
:如果条件不满足就抛出——用于内部错误。 -
require(bool condition)
:如果条件不满足就抛掉——用于输入或者外部组件引起的错误。Function bid()pubiie payable{ //拍卖未结束 require(now <=auctionEnd); }
-
revert()
:终止运行并回滚状态变动(无条件)
-
嵌套调用
Block header
汽油费的扣除机制
- ETH的三棵树都是在本地维护的数据结构
- 全节点收到对智能合约的调用,在本地账户扣除余额
- 然后将取得记账权的节点发布区块,将它本地维护的三棵树上传到区块链
ETH挖矿过程
- 全节点打包交易,执行对智能合约的调用,调整智能合约的内容
- 求得3棵树的根哈希值
- 试nonce,取得记账权,发布区块
- 别的没有取得记账权的区块,要独立验证新发布区块及其包含的tx和智能合约的合法性
应该先执行再挖矿(需要先算三棵树的hash再写进区块中)
如果不验证,无法更新本地三棵树的Hash值,无法发布正确的三棵树信息,无法挖矿
Receipt
问题:智能合约是否支持多线程?
不支持,状态机必须是完全确定,而多线程如果对内存访问顺序不同,造成的结果可能也会不同
出了多线程,“产生随机数”
智能合约可以获得的区块信息
block.blockhash(unint blockNumber) returns (bytes)
给定区块的哈希-仅对最近的256个区块有效而不包括当前区块
block.coinbase(address)
挖出当前区块的矿工地址
block.difficulty(uint)
当前区块难度
block.gaslimit(uint)
:当前区块gas限额
block.number(uint)
:当前区块号
block.timestamp(uint)
:自unix epoch起始当前区块以秒计的时间戳
智能合约可以获得的调用信息
msg.data(bytes)
:完整的calldata
msg.gas(uint)
:剩余gas
msg.sender(address)
:消息发送者(当前调用)
msg.sig(bytes4)
:calldata的前4字节(也就是函数标识符)
msg.value(uint)
:上随消息发送的wei的数量
now(uint)
:目前区块时间戳(block.timestamp)
tx.gasprice(uint)
:上交易的gas价格
tx.origin(address)
:上交易发起者(完全的调用链)
地址类型
<address>.balance(uint256)
以Wei为单位的地址类型的余额。
<address>.transfer(uint256 amount)
:
向地址类型发送数量为amount的Wei,失败时抛出异常,发送2300(转入账户的地址)gas的矿工费,不可调节。
<address>.send(uint256 amount)returns(bool)
:
向地址类型发送数量为amount的Wei,失败时返回false,发送2300gas的矿工费用,不可调节。
<address>.call(...)returns (bool)
:
发出底层CALL,失败时返回false,发送所有可用gas,不可调节。
<address>.callcode(...)returns(bool)
日
发出底层CALLCODE,失败时返回false,发送所有可用gas,不可调节。
<address>.delegatecall(...)returns (bool)
:
发出底层DELEGATECALL,失败时返回false,发送所有可用gas,不可调节。
所有智能合约均可显式地转换成地址类型
三种发送ETH的方式
<address>.transfer(uint256 amount)
<address>.send(uint256 amount)returns (bool)
<address>.call.value(uint256 amount)()
简单案例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;contract SimpleAuction {// 拍卖的受益人address payable public beneficiary;// 最高出价者address public highestBidder;// 最高出价uint public highestBid;// 拍卖结束时间uint public auctionEndTime;// 竞标是否已结束bool public ended;// 变更出价时触发的事件event HighestBidIncreased(address bidder, uint amount);// 拍卖结束时触发的事件event AuctionEnded(address winner, uint amount);// 构造函数,设置受益人和拍卖结束时间constructor(uint _biddingTime, address payable _beneficiary) {beneficiary = _beneficiary;auctionEndTime = block.timestamp + _biddingTime;}// 竞标函数function bid() public payable {require(block.timestamp <= auctionEndTime, "拍卖已结束");require(msg.value > highestBid, "已存在更高出价");if (highestBid != 0) {// 返还之前最高出价者的金额address payable previousBidder = payable(highestBidder);previousBidder.transfer(highestBid);}highestBidder = msg.sender;highestBid = msg.value;emit HighestBidIncreased(msg.sender, msg.value);}// 结束拍卖函数,只有受益人可以调用function auctionEnd() public {require(block.timestamp >= auctionEndTime, "拍卖尚未结束");require(!ended, "拍卖已结束");ended = true;emit AuctionEnded(highestBidder, highestBid);// 将最高出价转给受益人beneficiary.transfer(highestBid);}
}
参考文献
ETHASH 挖矿算法 — 以太坊的指南针 1.0.0 documentation (abyteahead.com)
虚拟机的执行资源 — 以太坊的指南针 1.0.0 documentation (abyteahead.com)