以太坊 Ethereum
以太坊(Ethereum)是一个支持智能合约的区块链平台,它与比特币最大的不同是,以太坊通过一个虚拟机(EVM)可以运行智能合约。
以太坊是Vitalik Buterin(维塔利克·布特林,人称V神)在2013年提出的概念,Vitalik最早参与了比特币社区的开发,并希望比特币把功能受限的脚本扩展成图灵完备的编程环境,但没有得到比特币开发社区的认同,于是他决定另起炉灶,打造一个新的区块链平台,目标是运行去中心化的程序。
以太坊在2015年开始运行,其中的原生货币是以太币Ether,简称ETH。一开始也是采用PoW进行挖矿,但是随着时间的推移,以太坊开始采用PoS进行挖矿。
和比特币区块链的区别
1. 账户模型
比特币使用的UTXO模型是一种对开发友好、易于实现清结算的模型,但对用户不友好,因为普通用户所认知的账户是一个账号、对应余额变动的模型。以太坊的账户模型和比特币不同,它就是余额模型,即交易引发账户余额的变动,这与传统金融账户一致。
2. 交易程序
之前介绍了比特币的支付原理,任何支付实际上都是在执行比特币脚本,只有脚本成功执行,支付才能成功。
以太坊的交易与之类似,并且更进一步,它实现了一个图灵完备的脚本语言,运行在 EVM(Ethereum Virtual Machine,以太坊虚拟机) 中,任何人都可以编写合法的脚本来执行任意逻辑(有很多限制),例如,定义一种新的代币,抵押贷款等。
账户
以太坊账户负责存储用户的以太坊余额。对大多数普通用户来说,以太坊账户和银行账户非常类似,通常只需要一个账户即可。
确切地说,以太坊账户分为外部账户和合约账户两类:
- 外部账户:即普通用户用私钥控制的账户;
- 合约账户:智能合约部署后生成的账户,一种拥有合约代码的账户,它不属于任何人,也没有私钥与之对应。
和比特币类似,一个以太坊账户就是一个公钥哈希后得到的地址,它是由一个私钥推导出对应的公钥,然后再计算出地址。其中,私钥与公钥算法与比特币完全相同,均为secp256k1椭圆曲线,但和比特币不同的是,以太坊采用非压缩公钥,然后直接对公钥做keccak256哈希,得到32字节的哈希值,取后20字节加上0x前缀即为地址。
可以看到账户地址就是公钥的前20字节加上0x前缀, 没有任何的校验,所以任何数字出错仍然可能是一个有效的地址。 为了防止这种情况,以太坊通过EIP-55实现了一个带校验的地址格式,大概过程:
original addr = 0x29717 bf 51 d 8 af c a 452459936d395668a576 b ce66
keccak hash = e72ec ce 2e b 2 ed 0 f fab5e05f043ee68fab3d f 759d...
checksum addr = 0x29717 BF 51 D 8 AF c A 452459936d395668A576 B ce66可以看到,就是使用这个摘要算法,对原地址做了一个hash处理,然后按位对齐,将哈希大于等于8(keccak中的哈希值)的字母变成大写。
NOTE
上面的hash都是16进制的,从0-9 a-f。
比如上面的keccakHasha中,e是≥8的,但是原地址对应位不是字母,所以不变;后面类推。
HD钱包
以太坊的账户和比特币账户采用的非对称加密算法是一样的,不同的只是公钥和账户地址的表示格式。所以比特币的HD钱包体系完全适用于以太坊。用户通过一套助记词,既可以管理比特币钱包,也可以管理以太坊钱包。
区块结构
比特币的区块链是由PoW保证每个区块都指向前一个区块,而在每一个区块内部,由一个独立的Merkle Tree来保证所有交易的不可篡改。用户的比特币是以UTXO的方式存储的,因此,比特币的交易就是不断地消耗现有的UTXO,并产生新的UTXO。
而以太坊采用的是账户模型,如果小明的账户在某个区块的资产是1 ETH,当小明给小红转账0.2 ETH后,刨除手续费,他的账户还剩下约0.8 ETH。由于小明的账户地址不变,所以,以太坊的区块结构必须能在每个区块持续地跟踪并记录小明的账户余额变动。因此,和比特币相比,以太坊的区块数据结构更加复杂。
Merkle Patricia Tree
以太坊存储账户数据的数据结构是MPT:Merkle Patricia Tree。是经过Merkle Tree改进来的。当MPT的每个叶子结点的值确定后,计算出的Root Hash就是完全确定的。和Merkle Tree相同的是,MPT的每个结点的值都是由它的子结点计算出来的。叶子节点一般都是交易信息,比如转账交易、合约创建交易等。
每个区块通过Root Hash将完全确定所有账户的状态。每个区块通过记录一个stateRoot来表示一个新状态。如果给定某个区块的stateRoot,我们肯定能完全确定所有账户的所有余额等信息。因此,stateRoot就被称为当前的世界状态。
如果第一个区块只有几个账户,随着账户的增加,如果有数百万个账户,到后面岂不是区块存储的数据量会越来越大?
实际上,每个区块的stateRoot表示的是一个完全状态的逻辑树,但每个区块记录的数据只包括修改的部分。但即使这样也会占用很大的内存,想要将一个有几百万节点的树完整地放入内存需要消耗大量的内存,而以太坊全节点也并不会将整颗逻辑树放入内存。实际上,每个节点的数据被存放到LevelDB中,节点仅在内存中存储当前活动的一些账户信息。如果需要操作某个不在内存的账户,则会将其从LevelDB加载到内存。如果内存不够,也会将长期不活动的节点从内存中移除,因为将来可以通过节点的路径再次从LevelDB加载。
账户数据
以太坊账户数据有以下几个部分:
- nonce:是一个递增的整数,每次交易就会加一,所以代表了交易次数。
- balance:账户余额,以
wei为单位,1Ether代表10^18个wei。之所以搞得这么大,是因为JS中小数计算是有误差的。 - storageRoot:如果和合约账户的话,这里存储合约相关的状态数据。如果是外部账户,为空。
- codeHash:是合约代码的hash。如果是外部账户,为空。
区块数据
一个以太坊区块由区块头和一系列交易构成。区块头除了记录parentHash(上一个区块的Hash)、stateRoot(世界状态)外,还包括:
sha3Uncles:记录引用的叔块;transactionRoot:记录当前区块所有交易的Root Hash;receiptsRoot:记录当前区块所有交易回执的Root Hash;logsBloom:一个Bloom Filter,用于快速查找Log;difficulty:挖矿难度值;number:区块高度,严格递增的整数;timestamp:区块的时间戳(以秒为单位);- ……
transactionRoot和receiptsRoot也是两个MPT树,但他两和stateRoot不同,他们仅表示当前区块的两棵树,与前面的区块状态无关。
UncleBlock 叔块
由于采用的是PoW共识机制,难免会有分叉的情况,在比特币区块链中,分叉后会采用最长链,其他链被抛弃;但是在以太坊中,分叉后还是采用最长链,但是会鼓励后续区块引用废弃的区块,这种被引用的废弃块就称为叔块。
一个区块可引用0~2个叔块,且叔块高度必须在前7层之内。
叔块的目的是给予竞争失败的矿工部分奖励,避免出现较长的分叉。
交易
在比特币中,交易就是消耗已有UTXO,并通过执行脚本产生新的UTXO,其中隐含的新旧差额即为矿工手续费。
在以太坊中,交易也需要手续费,手续费被称为Gas(汽油)。
以太坊除了最基本的转账:即从一个账户支付Ether到另一个账户,还支持执行合约代码。合约代码是图灵完备的编程语言,通过EVM(以太坊虚拟机)执行。
为了保证合约代码的可靠执行,以太坊给每一个虚拟机指令都标记了一个Gas基本费用,称为gasUsed。例如,加减计算的费用是3,计算SHA3的费用是30,输出日志的费用是375,写入存储的费用高达20000。总的来说,消耗CPU比消耗存储便宜,简单计算比复杂计算便宜,读取比写入便宜。
除了gasUsed外,用户还需要提供一个gasPrice,以Gwei(1Gwei=109Wei)为单位。通过竞价得到一个矿工愿意接受的gasPrice。如果一个交易消耗了120000的gasUsed,而gasPrice是50 Gwei,则交易费用是:
120000 x 50 Gwei = 6000000 Gwei = 0.006 Ether但是在执行代码的时候,存在条件判断、循环等语句,同一段代码,执行的结果也可能不同,因此,事前预计一个合约执行要花费多少Gas,不现实。
所以以太坊规定,一笔交易,先给出gasPrice和gasLimit,如果执行完成后有剩余,剩余的退还,如果执行过程中消耗殆尽,那么交易执行失败,但已执行的Gas不会退。
举个例子来理解一下: 假定某个账户想执行一笔交易,他给出gasPrice为50 Gwei,预估gasUsed约120000,设定gasLimit为150000,则预支付的Ether为:
150000 x 50 Gwei = 7500000 Gwei = 0.0075 Ether如果账户的Ether余额不足0.0075,则该交易根本无法发送。如果账户余额等于或超过0.0075,例如余额为0.008,则矿工可以将该交易打包。假设实际执行消耗的gasUsed为120000,则交易费用0.006,账户剩余0.002。
很少有交易能准确预估gasUsed,只有标准转账交易是21000,因此,标准的转账交易gasLimit可以设置为21000(即恰好消耗完毕无剩余)。
Gas Price是全网用户竞价产生的,它时刻在波动。如果交易少,Gas Price将下降,如果交易多,网络拥堵,则Gas Price将上升。以太坊的Gas价格可以在Etherscan跟踪。
NOTE
ERP1559之后交易费用Gas Fees由 gasPrice * gasLimit 改为了Base + Max + Max Priority三个部分。
Base 每次交易需要付出的,会烧掉,任何人都不会持有 Max 交易需要给矿工节点的 Max Priority 单独给矿工节点的,希望来加快交易速度
交易回执
以太坊区块为每一笔交易都会产生一笔回执(Recipt),表示交易的最终状态。一个回执信息主要包括:
- status:执行结果,1表示成功,0表示失败;
- gasUsed:已消耗的Gas数量;
- txHash:交易Hash;
- logs:交易产生的日志;
转账交易
- Transaction Hash: 0xb940...4ad7,这是交易Hash,即交易的唯一标识;
- Status: Success,表示交易成功;
- From: 0x0FFf...bBc4,交易的发送方;
- To: 0x5b2a...5a46,交易的接收方;
- Value: 1.6912 Ether,交易发送的Ether;
- Gas Price: 82 Gwei,Gas的价格;
- Gas Limit: 21,000,转账交易恰好消耗21000Gas,因此总是21000;
- Usage by Txn: 21,000 (100%),消耗的Gas占比,这里恰好全部消耗完;
- Nonce:0,发送方的nonce,0表示第1笔交易;
- Input Data: 0x,因为是转账交易,没有输入数据,因此为空。
合约交易
合约交易就是指一个外部账号调用某个合约的某个public函数。看这笔交易:
- From: 0x2329...BA3a,交易的发起方,该地址一定是外部账户;
- To: 0x7a25...488D,交易的接收方,这里地址是一个合约地址;
- Value: 4.5 Ether,即向合约发送4.5 Ether;
- Gas Limit: 152,533,这是交易发起前设定的最大Gas;
- Usage by Txn: 125,290 (82.14%),这是交易实际消耗的Gas;
- Input Data: 0x7ff36ab5...,这是交易的输入数据,其中包含了调用哪个函数,以及传递的参数,解码后可知调用函数是swapExactETHForTokens。
可见,转账交易的Gas费用是固定的,而合约交易只能预估,具体费用以实际执行后消耗的为准。
智能合约Smart Contract
以太坊相比比特币的一个重大创新就是它支持智能合约(Smart Contract)。
智能合约,就是一种运行在区块链上的程序。和普通程序不同的是,智能合约要保证在区块链网络的每一个节点中运行的结果完全相同,这样才能使任何一个节点都可以验证挖矿产出节点生成的区块里,智能合约执行的结果对不对。
因此,以太坊提供了一个EVM(Ethereum Virtual Machine)虚拟机来执行智能合约的字节码,并且,和普通程序相比,为了消除程序运行的不确定性,智能合约有很多限制,例如,不支持浮点运算(因为浮点数有不同的表示方法,不同架构的CPU运行的浮点计算精度都不同),不支持随机数,不支持从外部读取输入等等。
一个智能合约被编译后就是一段EVM字节码,将它部署在以太坊的区块链时,会根据部署者的地址和该地址的nonce分配一个合约地址,合约地址和账户地址的格式是没有区别的,但合约地址没有私钥,也就没有人能直接操作该地址的合约数据。要调用合约,唯一的方法是调用合约的公共函数。
NOTE
合约被部署后将自动获得一个地址,并可像外部账户一样存取Ether,还可以存储状态数据;
这也是合约的一个限制:合约不能主动执行,它只能被外部账户发起调用。如果一个合约要定期执行,那只能由线下服务器定期发起合约调用。
此外,合约作为地址,可以接收Ether,也可以发送Ether。合约内部也可以存储数据。合约的数据存储在合约地址关联的存储上,这就使得合约具有了状态,可以实现比较复杂的逻辑,包括存款、取款等。
合约在执行的过程中,可以调用其他已部署的合约,前提是知道其他合约的地址和函数签名,这就大大扩展了合约的功能。 例如,一个合约可以调用另一个借贷合约的借款方法,再调用交易合约,最后再调用还款方法,实现所谓的“闪电贷”(即在一个合约调用中实现借款-交易-还款)功能。
常用合约
- ERC-20:以太坊标准代币合约;
- Wrapped Ether:将以太坊封装为ERC20的合约;
- ERC-721:以太坊NFT标准合约;
- ERC-1155:相同NFT允许多个持有者的合约。