区块链-以太坊笔记
比特币系统:基于交易
以太坊:基于账户(160位,即40位16进制数),对与double spending attack有天然防御作用,不用说明币的来源
账户:
1)外部账户:账户余额(blance)、交易次数(nouce:用来防止重放攻击,每次交易的时候先检查该交易是否已执行)
1)合约账户:不能直接发布交易,一个合约可以调用另一个合约,除了有账户余额与交易次数外,还有代码(code)和存储状态(storge)
以太坊的数据结构:
1)状态树:键值对的映射,地址——>状态(key,value),状态时经过RLP(Recursive Length Prefix:只支持可嵌套的字节数组(nested array of bytes))编码序列化后再保存
前缀树(trie)
特点:1、因为是40位16进制数(0~f),所以每个结点的分叉数目最多为17个(16个分支+一个表示结束的标志位,标志位表示到该位置该账户地址是否结束)
2、地址()的查找效率,与地址的长度有关
3、trie不会出现碰撞
4、任一账户插入到状态树中,最终构成的状态树都唯一
5、更新操作的局部性更好,比如要更新某个账户的状态,就只需访问该账户在状态树中对应的分支即可
缺点:浪费内存
压缩前缀树(Patricia trie):经过路径压缩的前缀树
Merkle Patricia trie:具有哈希指针的压缩前缀树,交易时可用于证明一个一个账户上有多少钱
而以太坊中用到的是修改过的压缩前缀树(Modified MPT):

区块链上的每个区块的状态树是共享的,只有某些账户状态发生变化时需要进行修改,状态没变化的账户一般是指向前一个区块的状态树上的对应账户
合约账户的存储也是用一棵小的MPT
区块链上都要保存历史状态,其目的是保证回滚操作的正常进行,因为有智能合约存在,不保存历史状态,无法进行回滚操作
状态树包含每一个账户的状态
交易树是一个区块内的所有交易组成的一棵树,可以用来证明一个交易被打包到区块中了
bloom filter: 用来支持一种复杂的查找方法(比如查找所有与某个智能合约相关的交易,将所有的交易通过几组hash函数映射到一个hash表中,直接在hash表中查找相关交易)
特点: false positive,比如查找某个集合中是否存在某个元素,元素在集合中就一定能找到,但也可能有其它元素与该元素映射到相同的地址中,造成误报。
缺点:普通的bloom filter不能进行删除操作,因为有hash碰撞,删除的可能不只一个元素
每个交易执行完之后会有一个收据,收据中包含了bloom filter,bloom filter包含了类型,地址等信息
发布的区块在它的块头中有一个总的bloom filter,这个bloom filter是所有交易中的bloom filter的并集
状态树,交易树,收据树的根哈希值都是包含在区块的块头里的
以太坊的运行过程可以看作是一个交易驱动状态机(transaction-drive state mechine)
GHOST协议
以太坊的出块时间降到了十几秒,因此可能出现几个结点同时有记账权的情况,同时又存在最长合法链,由于大型矿池的出现,可能导致某些结点区块白挖,大型矿池的收益大于其算力的情况。
以太坊最初是这样来解决问题的:
比如链上同时出现了三个分叉,其中一个分叉已经胜出,即其区块后已经有了新的区块连接(已经成为最长合法链),则另外两个区块就是胜出区块后连接的新区块的叔父区块(uncle block,一个区块最多只能有两个uncle block),两个uncle block都能得到(出块奖励*7/8)的收益,而新区块只要有一个uncle block就能额外得到 (1/32 * 出块奖励),最多可以有两个uncle block,即一个区块最多可额外得到 (2/32 * 出块奖励 )的收益。新区块要包含uncle block才能得到额外的出块奖励,如果一个区块没有uncle block,那就没有额外的出块奖励
该设计主要是为了鼓励区块链上出现分叉后及时合并
改进:1、一个区块最多可以有两个uncle block,一个uncle block的额外奖励是 (1/32*出块奖励)
2、一个区块只有7代之内的叔父区块有效,即一个区块最多只能有6个等级(辈分)的叔父区块,离区块最近的叔父区块被包含在后代区块中可得 (7/8 * 出块奖励)离其被写入的区块越远,则收益依次递减,第二个叔父区块只得(6/8 * 出块奖励),... ,离得最远的叔父区块只得(2/8 * 出块奖励)。也就是说如果同时出现超过两个叔父区块,如果没有被离得最近的后代区块包含,也可以被该后代区块的后代区块包含。
区块上链结点得到的所有收益 = block reward + gas fee //发布智能合约要付出gas fee,而执行智能合约的矿工会得到gas fee)
以太坊的出块奖励不同于比特币,比特币的出块奖励会定期减半,最终趋于0,而以太坊的出块奖励与挖矿难度调整有关。
只有分叉后的第一个区块可以得到uncle reward
挖矿算法
ASIC矿机:与普通计算机比,计算能力强,但在内存访问的性能上没有很大优势
莱特币:设计的初衷是ASIC Resistance,memory hard mining puzzle(增加puzzle对内存访问的需求),其puzzle是基于scrypt(一个hash函数)
挖矿过程:先填充数组:根据一个种子seed,取hash后生成一个伪随机数填入数组第一个位置,后续位置都是前一个位置的值取hash得到,这样一个数组中的值是有关联的,可以验证。按伪随机的顺序读取数组中的元素来进行散列运算。先读取数组中的一个元素,然后对该数进行迭代更新操作得到下一个要读取的位置,在对该位置的数进行迭代更新操作获得下一个要读取的位置
与比特币的区别:其出块速度是比特币的4倍,计算puzzle的方式不同(many puzzle)
比特币puzzle的设计思想:defficult to solve, but easy to verify.
缺点:挖矿的结点必须要保存cache,以便于读取操作,而轻结点也必须要保存cache,才能进行验证操作。为了顾及轻结点,最终导致其cache实际的大小仅为128k,并没有完全实现ASIC Resisitance。
ETH:
cache: 初始大小为16M, 先通过扫描区块头生成一个种子seed,计算该seed的hash值并放入cache的第一个位置,在cache中,每一个值都是其前一个值取hash得到的,比如第二个位置的值就是第一个位置的值取hash算出的,cache中的值有依赖关系,依次计算填充cache每一个位置的值。每隔30000个块会重新生成seed,并重新生成cache,cache的大小也要增长原始cache的1/128,即128k
dataset:初始大小为1G,dataset是基于cache生成的。先从cache中读取一个数,通过该hash值计算得到下一个要读取的位置,如此在cache中读取256个数后得到的hash值存在dataset的第一个位置,dataset中的其他位置的数都是在cache中读取256个数后生成的hash值,进行这样的操作生成dataset。计算puzzle的时候使用dataset,不使用cache,dataset也是每隔30000个块更新,同时增长原始dataset的1/128,即8M
挖矿过程:通过区块头中的nonce值计算出一个初始的hash值,映射到dataset中的一个位置,同时要读取其下一个相邻位置的元素,即每次映射读取两个元素,通过对映射的位置进行运算得到下一个要读取的位置,同时读取其相邻元素,如此循环64次,即读取128个数,最终得到一个hash值,再与puzzle的目标阈值比较,如果不符合目标阈值,再将block header中的nonce替换一下,再进行以上操作,直到找到符合目标阈值的nonce为止。
挖矿使用的是dateset,而验证使用的是cache,这样设计的原因是方便只存储cache的轻节点能验证区块的正确性(验证的时候用cache生成dataset相应位置的hash值)
难度调整



权益证明
目前比特币和以太坊都是使用基于工作量证明(proof of work),POS的一大痛点就是挖矿需要消耗电能
基于股权证明(proof of stake):保留一部分货币给开发者,出售部分货币来获得开发该加密货币所需的资金,其共识机制是由个人拥有货币的数量来进行投票的,是一个虚拟挖矿过程(virtual mining),减少了能耗与对环境的影响。该类权益证明类似于股份制公司
在基于工作量证明的系统中,可以通过大量购买挖矿设备来增加算力,对对应系统进行攻击,特别是对刚刚诞生的新币种(AltCoin )而言,这种攻击(Infanticide)是灾难性的;而在基于权益证明的系统中,如果有节点想发起恶意攻击,就必须先大量购入货币,从而会引起该类加密货币的升值,有恶意的节点就会付出更大的代价。这对开发者而言就不一定是坏事了。
POS与POW并不是互斥的,以太坊未来的发展方向就是采用POS+PSW的模式。
POS+POW的早期设计方法:挖矿难度与所持有的货币数量相关,持有货币数量越多,挖矿难度越低,即可以通过注入货币来降低挖矿难度。规定注入的货币在降低了挖矿难度之后,需要过一段时间,或者说是经过一定区块过后才能被再次使用(proof of deposit)。该规定主要是为了约束持有货币量多的节点,保证相对公平地进行挖矿。但该设计方法遇到的一个问题是在产生分叉的时候,节点在一条分叉上使用货币进行难度降低后,这部分货币在该分叉上是被冻结的,但在另外的分叉上是能重新使用的,节点在产生分叉的地方出现了"两边下注"的现象(nothing at stake),即在两条分叉上都挖矿。
以太坊过渡阶段的权益证明(Casper the Friendly Finality Gadget(FFG)):POW+POS。节点仍然要挖矿,但挖出100个区块后要进行一次Finality,确定最长合法链
验证者(validator): 节点通过提交部分以太币作为保证金来成为验证者,验证者的存在是为了推动系统达成共识,通过投票来决定哪条链是最长合法链,投票的权重与保证金的数目大小成正比
具体过程:
two-phase commit:1、prepare message; 2、commit message
1、每50个区块为一个epoch,进行一次投票,前一个epoch对后一个epoch来说是prepare message, 而后者对前者来说是commit message
2、连续两个epoch都得到2/3的投票数量,证明该次验证有效
3、经过两轮投票决定最终最长合法链
如果因某个验证者行政不作为,迟迟不投票,导致不能及时达成共识,该验证者就会被扣除部分保证金
如果验证者乱作为,给有冲突的两个分叉都投票,该验证者就会被没收全部保证金,被没收的保证金会被销毁
验证者有任期,在任期满了之后,有一段时间的等待期,等其它节点检举揭发该验证者是否有不良行为,进行惩处,等待期结束后,如果没有问题,验证者可以取回自己的保证金和相应的奖励
EOS的权益证明方式:Delegated Proof of Stake
智能合约
合约的调用
交易只能由外部账户发起,合约账户不能自己发起交易。所以是由外部账户来调用合约账户合约中的函数,具体调用合约账户中的哪个函数,在交易的数据域(data域)中说明
一个合约调用另一个合约中的函数
首先需要外部账户发起交易,调用一个合约,该合约再调用另一个合约中的函数
合约调用另一个合约的方式
1、直接调用
e.g 比如是B合约调用A合约,在A合约中定义一个事件,并在A合约的函数中用emit调用该事件
B合约中的函数以A合约的地址做参数,先生成一个A合约的实例,通过该实例调用A合约中的函数,从而触发A合约中的事件,完成调用
缺点:该种调用方式,如果执行A合约中的函数调用过程中出错,则B合约中的函数也将抛出异常,本次调用全部回滚
2、使用address类型的call函数:call函数的第一个参数是要调用函数的签名,第二个参数是要调用函数的参数
e.g address.call(参数1,参数2) //address是合约A的地址
该种调用方式相当于是切换到合约A的环境变量中进行函数调用,如果合约A中函数调用失败,call会返回false,不会影响到合约B中函数的执行
3、代理调用delegatecall()
使用方法与call()相同,区别在于不需要切换到合约A的上下文环境中
合约账户如果要能接收外部转账,则必须标注成payable
e.g如果调用合约账户中的函数,向合约账户中转入以太币,如果该函数没有标注成payable,则会抛出异常
fallback()函数
在两种情况下会被调用:
1、直接向一个合约地址转账而不加任何data(即data域中不指明调用的是哪个函数)
2、被调用的函数不存在
转账金额不为0,同样需要标注payable,否则调用该函数时会抛出异常
fallback()函数不是必须定义的,但如果有以上两种情况且fallback函数没有定义,调用就会抛出异常
智能合约的创建和运行
智能合约的代码写完后,编译成bytecode
由一个外部账户发起一个转账到0X0地址的交易,转账金额是0,但要支付汽油费,智能合约代码就放在data域中,然后该交易就被发布到区块链上
缺点:智能合约的规则是由代码逻辑决定的,一旦发布的区块链上,就没有人能更改了,但一但智能合约设计得不好,有bug,也没有办法对该智能合约进行更正
智能合约运行在EVM上
智能合约中定义了单位汽油量的价格(Price),以及该合约愿意支付的最大汽油量(GasLimit),执行合约时先扣除该合约愿意支付的最大汽油费,执行完合约后,在算出实际消耗的汽油费,如果汽油费有剩余则退回,如果汽油费不够则会引起回滚操作。该方法解决了智能合约可能出现的死循环问题
交易具有原子性:要么全部执行,如果交易过程抛出异常,则会回退到交易执行前
如果智能合约执行过程发生异常,则已执行的合约消耗的汽油费是不会退还的
错误处理
assert(bool conditon) //条件不满足,抛出错误--用于内部错误
require(bool condition) //条件不满足,抛出错误--用于输入和外部组件引起的错误
revert() //无条件终止运行并进行回滚操作
在区块头中的GasLimit是指该区块所有交易能消耗的汽油费的限额,相当于比特币系统中约束一个区块的大小不超过1M一样,用于约束以太坊中的区块对资源的消耗
矿工在发布区块的时候可以在上一个区块的GasLimit的基础上,上调或者下调1/1024
每个全节点本地维护着状态树,交易树,收据树,在执行智能合约的时候,每个全节点运行该智能合约,在自己维护的本地状态树上扣去最大汽油费即可
每一个新区块发布之后,全节点执行该新区块,更新自己本地的三棵树
挖矿的过程是先执行交易,包括交易中的智能合约,得到完整的Block Header,再进行挖矿,即计算nouce值
挖矿时矿工执行智能合约需要消耗资源,但执行智能合约的汽油费只有挖到矿的矿工才能获得,其他挖矿失败的矿工或者进行验证的全节点执行智能合约过程中消耗的资源不会得到补偿
发布到区块链上的交易不一定都是执行成功的,因为交易中如果包含智能合约,则需要扣除汽油费,必须要将交易发布到区块链上,才能得到汽油费。收据树上的Status域会标识该交易的执行状况
智能合约转账


tranfer和send都是专门用来转账的函数,区别在于tranfer函数调用出错会抛出异常,引发回滚操作,而send函数会返回false,不会影响调用该函数的合约的执行
call操作本义是进行函数调用,但也可用于转账操作,区别在于tranfer和send函数只发送2300的汽油费,而call函数是发送该合约所有的汽油费
智能合约不支持多线程并发执行
The DAO:去中心化的一个类似众筹的组织
用户发送以太币到The DAO上,换得代币,The DAO有了收益后再对相应用户进行回馈
由于The DAO的代码逻辑有漏洞,被黑客发起了重入攻击,从中盗取了大量以太币,幸运的是The DAO中的以太币还需28天之后才会转到黑客的账户上,给了以太坊开发社区采取补救措施的时间。
补救措施:
1、采用软分叉,发布一个软件升级,所有与The DAO相关的账户地址不能做任何交易,就是在发布到区块链上之前先检查交易是否合法,可惜的是这个检查交易是否合法的过程是不收取任何汽油费的,许多矿工升级了软件之后,收到大量不合法的交易,执行检查交易的过程要浪费矿工的资源,于是许多矿工又回退到了软件升级之前的版本,第一次补救失败。
2、发布了软件升级,创建了一个新的智能合约,强制性地将The DAO中的所有资金转到这个新的智能合约中,写明了在执行到192万个区块的时候,自动执行这个转账交易,不需要合法的签名,这个智能合约再将钱回退给The DAO中的各个账户,不过有部分用户不同意以太坊开发社区干涉区块链,随便写一个智能合约(而没有签名)就能进行转账这个行为,这部分用户不认同这个补救措施,也就是不认同这个转账交易相关的区块(没有合法签名,是非法交易),最终导致了硬分叉,以太坊分裂成了两条链,执行了这个转账交易的链是ETH,而不认同的用户继续挖矿的链是ETC(经典以太坊)。
美链
美链是一个部署在以太坊上的智能合约(交易所),有自己的代币,代币可与以太币进行兑换,在美链中可以拥有自己的代币账户,用于接收代币。
类比场景:公司给每个员工发送数目相等的一笔工资。公司是发送者,每个员工都是接送者。发送者先计算要发送的工资的总额:account = 员工数目 * 每个员工要发的金额(value),确定自己的余额足够发放工资。后分两步,1)自己的余额中减去要发放的工资数目(account);2)给每一个员工发放工资(value)
漏洞:计算account时由于是乘法运算,可能发生溢出,即算出的account会变成一个很小的值。导致每个员工收到相应数目的工资,但公司总金额确没有减少的问题。类似于是导致了区块链系统中凭空增加了部分代币数量。
解决办法:不要直接使用乘法来进行计算,而是调用Solidity的SafeMath库中的函数来进行运算,该库中的函数会进行溢出检查。

浙公网安备 33010602011771号