读《以太坊技术详解与实战》笔记
读《以太坊技术详解与实战》笔记)
整体目录
第一章
理解区块链
定义:区块链是一种分布式,去中心化的计算与存储架构
区块链解决什么:如何用一种可信赖的方式记录数据,使得用户可以信任区块链系统记录的数据而无需假设节点的可信性
三个部分:区块链的数据结构、分布式存储、一致性协议
- 数据结构
- 分布式存储
系统中各个节点组成一个P2P网络,每个节点均分别执行、验证和记录相同的交易,每个节点都可以在本地存储完整的区块链数据
- 一致性协议
- POW
- POS
- DPOS
- EOS
以太坊
- 一种能够被重编程用以实现任意复杂计算功能的单一区块链
- 用户可以直接开发自己的区块链应用,而无需担心底层的区块链系统
- 对比比特币的优点
- 更快的的出块速度以及更先进的奖励机制
- 以太坊支持智能合约,用户可以自定义数字资产和流通逻辑,可以执行任何计算,但是比特币只支持转账
- 以太坊发送的交易不仅仅是金额还可以是调用一段代码
智能合约
- 合约创建
- 合约调用
应用场景
- 时间戳和溯源:由于数据区块持续增长且不可篡改,所以可以证明过去某个时间发生过某件事
- 数字资产的发行和流通:采取统一标准(如ERC20)使得不同商户资产实现流通,比如Q币与支付宝流通
- 跨组织的数据共享:涉及多家方参与的项目,利用检测自动完成交易或则违约支付
DAPP
- 去中心化应用( Decentralized Application, DA pp )是一种运行在去中心化点对点(P2P) 网络上的应用软件。
- 相比于现有中心化软件优点
- 项目开源,公开透明
- 去中心化
- 具有激励机制
实例
- Golem
- CryptoKittes
- Augur
- Bancor
- KyberNetwork
第二章
以太坊整体架构
- 底层服务
- P2P网络服务
- LevelDB数据库
- 密码学算法
- 分片(SHarding)优化
- 核心层
- 区块链
- 共识算法
- 以太坊虚拟机
- 顶层应用
- API接口
- 智能合约
- 去中心化应用
- 概括
底层服务中LevelDB数据库存储了交易、区块等数据,密码学算法为区块的生成、交易的传输等进行加密,分片优化加快了交易验证的速度,共识算法用于解决P2P网络节点之间账本的一致性,顶层应用中的去中心化应用需要在以太坊虚拟机上执行,各层结构相互协同又各司其职,共同组成一个完整的以太坊系统
区块
比特币区块
以太坊区块
两张图对比学习
账户
用户帐户
- 地址
- 余额
智能合约账户
智能合约是由事件驱动的、具有状态的、运行在一个可复制的、共享的账本之上的计算机程序,当满足特定条件时,智能合约会自动执行。合约一旦部署不可修改、合约执行后不可逆、所有执行事务可追踪。
- 地址
- 余额
- 状态:智能合约中声明的所有变量和变量的当前状态
- 代码
地址是账户唯一标识
智能合约能调用其他智能合约
公钥和私钥
32byte=256bits
1 | 第一步:私钥 (private key) |
数据结构与储存
- 数据组织形式
- Merkle Patircia树(MPT)
- Merkle树
- Trie树
- 状态树:代表访问区块后的整个状态
- {nonce,balance,code,stroge}
- 交易树:键值对,每个键是交易编号,值是交易内容
- 收据树:RLP编码的数据结构 [medstate,Gas_used,logbloom,logs(address,[topic1,topic2])]
数据库支持
共识机制
POW
工作结果证明你完成相应的工作
POS
基于网络参与者目前持有的数字货币的数量与时间进行利益分配
拜占庭将军问题
以太币
ETH
- 1eth=1e^18wei
交易
- from:交易发送者的地址,必填
- to:交易接收者地址,空则代表合约创建交易
- value:发送者要转移的以太币数量
- data:存在的数据字段,存在代表交易是创建合约或调用合约交易
- Gas Limit:表示交易允许消耗的最大Gas数量
- GasPrice:表示发送者愿意支付给矿工的Gas的价格
- nonce:用来区别同一用户发出的不同交易标记
- hash:以上信息生成的散列值,作为交易ID
- r、s、v:交易签名,发送者的私钥对交易hash进行签名生成
以太坊交易(支持三种data区分)
价值传递
- TO :收款地址
- DATA :留空或留言信息
- FROM :谁发出
- AMOUNT :发送多少
1 | web3.eth.sendTransaction({ |
创建合约
- TO :留空 (这就是触发创建智能合约的原因)
- DATA :包含编译为字节码的智能合约代码
- FROM :谁创建
- AMOUNT :可以是零或任何数量的以太,它是我们想要给合约的存款
1 | web3.eth.sendTransaction({ |
调用合约函数
- TO: 目标合约账户地址
- DATA: 包含函数名称和参数 - 标识如何调用智能合约函数
- FROM :谁调用
- AMOUNT : 可以是零或任意数量的以太,例如可以支付合约服务费用
1 | web3.eth.sendTransaction({ |
数据编码与压缩
客户端与API
以太坊域名服务
ENS:使用户注册支持智能合约运行的域名和利用底层设 备标识符解析部分域名
通过竞拍获取
第三章
区块链类型
- 私有链:指写入权限在某一个组织控制下的区块链,读权限可以公开或加以先知
- 联盟链:共识过程受到预选节点控制,由不同实体分别控制节点形成授权网络,非联盟节点,只能读取,无权参与共识达成过程
- Quorum
- 公有链:主网和测试网,适用于虚拟货币、面向大众的电子商务
安装部署以太坊
此处与书中不同,采取图形化软件Ganache自动化部署
使用参见官方文档
第四章
智能合约
以太坊和智能合约本身只是一个工具,其具体实现的功能和特性由企业和开发者决定 理论上讲,任意计算复杂度的金融交互过程均可以由智能合约安全 自动地完成 除了金 融方面的应用,以太坊平台还可以在如财产登记、投票、智能交通、物联网等任何需要信 任、安全和性能兼顾的环境中进行部署和使用
创建和调用
适用语言
- Solidioty
- Serpent
- LLL
集成开发环境
Remix
存储方式
- 栈:以太坊虚拟机的底层运行机制
账户储存:作为账户的一个属性保存,与影片一样
内存:以太坊虚拟运行代码时临时分配的一块线性空间,合约调用结束自动释放
指令集
消息调用
solidity
第五章
Remix
第六章
投票(代码与原版有所差异)
1 | pragma solidity ^0.4.0; |
1.revert()、assert()、require()三者的区别
1 | if(msg.sender != owner) { throw; } <^0.4.13 |
- 函数
assert
和require
可用于检查条件并在条件不满足时抛出异常。 assert
函数只能用于测试内部错误,并检查非变量。require
函数用于确认条件有效性,并提供一个字符串消息revert
函数可以用来标记错误并恢复当前的调用。调用包中有关错误的详细信息返回给调用者
Solidity 对一个 require
式的异常执行回退操作(指令 0xfd
)并执行一个无效操作(指令 0xfe
)来引发 assert
式异常。想要保留交易原子性,最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果
2.memory与storage区别
storage | memory | |
---|---|---|
储存的变量 | 函数外部声明的变量,即状态变量 | 函数内部声明的变量,即局部变量 |
存储的位置 | 区块链上,永久存在 | 内存中,运行完之后销毁 |
运行的位置 | 区块链网络上 | 单个节点 |
传递属性 | 指针传递 | 值传递 |
两者使用成本与calldata
storage
存储中的数据是永久存在的。存储是一个key/value库
存储中的数据写入区块链,因此会修改状态,这也是存储使用成本高的原因。
占用一个256位的槽需要消耗20000 gas
修改一个已经使用的存储槽的值,需要消耗5000 gas
当清零一个存储槽时,会返还一定数量的gas
存储按256位的槽位分配,即使没有完全使用一个槽位,也需要支付其开销
memory
- 内存是一个字节数组,槽大小位256位(32字节)
数据仅在函数执行期间存在,执行完毕后就被销毁
- 读或写一个内存槽都会消耗3gas
- 为了避免矿工的工作量过大,22个操作之后的单操作成本会上涨
calldata/调用数据
- 调用数据是不可修改、非持久化的区域,用来保存函数参数,其行为类似于内存
- 外部函数的参数必须使用calldata,但是也可用于其他变量
- 调用数据避免了数据拷贝,并确保数据不被修改
- 函数也可以返回使用calldata声明的数组和结果,但是不可能分配这些类型
但是本合约中的多数函数需要使用storage来防止一些危险的因数,防止投票者的一些状态丢失
3.函数权限关键字与修饰关键字
函数关键字
- public:只有 public 类型的函数才可以供外部访问,当一个状态变量的权限为 public 类型时,它就会自动生成一个可供外部调用的 get 函数。当函数声明时,它默认为是 public 类型,而状态变量声明时,默认为 internal 类型。
- private:只能在当前类中进行访问,子类无法继承,也无法调用或访问。
- internal:子类继承父类,子类可以访问父类的 internal 函数,同时,使用 using for 关键字后,本类可以使用被调用类的 internal 函数。
- external:被声明的函数只能在合约外部调用。
修饰关键字
- modifier:被 modifier 关键字声明的关键字所修饰的函数只能在满足 modifier 关键字声明的关键字的要求后才会被执行。
- constant:被声明为 constant 的状态变量只能使用那些在编译时有确定值的表达式来给它们赋值。任何通过访问 内存、链数据(例如 now,this.balance 或 block.number)或执行数据(msg.gas)或对外部合约的调用来给它们赋值都是不允许的。不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。
- view:被该关键字修饰的状态变量只能读取其值,不能对该状态变量的值进行修改。
- pure:被该关键字修饰的状态变量既不能读取变量,也不能修改该变量。
4.变长字节数组
- 一个元素类型为
T
,固定长度为k
的数组可以声明为T[k]
,而动态数组声明为T[]
- 数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反 ,T[0]代表最后一个元素
.lenth
表示当前数组的长度。储存在storage的动态数组可以通过修改.lenth
修改数组大小,memory的数组长度是固定的- 变长的 存储(storage) 数组以及
bytes
类型(而不是string
类型)都有一个叫做push
的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度
5.映射键值对
映射
或字典类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)
。键的类型允许除映射
外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。
简单来说,映射就是一个哈希表,每一个key与一个value互相对应,通过知道键值可以快速地定位到value
但是我们并不存储键的数据,仅仅存储它的keccak256
哈希值,用来查找值时使用。
6.tips
solidity
调用栈最深为1024
,尽量用循环
拍卖
1 | pragma solidity ^0.4.22; |
1.事件
事件是能方便地调用以太坊虚拟机日志功能的接口。
- 定义事件
event EventName(address bidder, uint amount);
- 触发事件
emit EventName(msg.sender, msg.value);
- 本合约使用日志功能记录不同地址的不同出价,记录后我们可以搜索事件
- 其中有一个参数修饰是 indexed ,用来表示这个参数用作索引,查询日志时就可以根据这个索引进行过滤
- 事件搜索
2.函数返回值
使用返回变量名
1
2
3
4
5
6function arithmetic(uint _a, uint _b) public pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}直接在return语句中提供返回值
1
2
3
4
5function arithmetic(uint _a, uint _b) public pure
returns (uint o_sum, uint o_product)
{
return (_a + _b, _a * _b);
}Getter 函数
所有定义为public的状态变量都有getter函数,由编译器自动创建。该函数与变量具有相同的名称,并且具有外部可见性
3.payable
如果在函数中涉及到以太币的转移,需要使用到payable关键词。意味着可以在调用这笔函数的消息中附带以太币。
4.重入攻击-Re-Entrancy
以太坊上的智能合约彼此之间可以相互调用。假设在一个合约A执行过程中发生了一次外部的合约B调用,并且合约B是由黑客所控制的,合约B的调用过程中可以重新进入合约A的调用。如果合约A在执行外部合约调用之前并未完成自己的内部状态更新,则有可能会被合约B利用从而盗取资产。
先分析DAO攻击中的问题代码
1 | function withDraw(){ |
该函数的功能是实现提款操作。逻辑顺序是,先执行退款操作,再将账户的余额进行相应扣除。
首先由于Gas的限制,不需要担心死循环。但是以太币转账会触发代码执行,如果接收方是智能合约,那么就能在接受的过程中再次调用withdraw()函数。
- 回退函数(fallback function)
每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether
时(没有任何其它数据),这个函数也会被执行。
如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,终止循环结束(以太坊 gas 有上限)
1 | function() payable{//定义payable修饰使得fallback函数具备转账功能 |
本合约中的withdraw函数编写符合退款逻辑,避免了重入攻击
1 | function withdraw() public returns (bool) { |
把写操作放在外部函数调用之前:先扣除在进行转账
修复建议
- 变成“先记录,后转账”的模式-(Checks-effects-interactions)
- 采用
transfer()
函数进行转账,或采用to.call.gas(2300).value(amount)();
函数对gas
进行限制。 - 采用锁机制
1 | modifier onlyUnlocked{ |
modifer
```java
//定义修饰器modifier modifierfun(uint value){ require(value >= 10); _; //代表修饰器所修饰函数中的代码。 } // 修饰器修饰函数。 (先执行修饰器中的代码,再执行函数中的代码) function setValue(uint num) modifierfun(num){ a = num; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
### 盲拍
```java
pragma solidity >=0.5.0 <0.7.0;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// 可以取回的之前的出价
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
/// 使用 modifier 可以更便捷的校验函数的入参。
/// onlyBefore 会被用于后面的 bid 函数:
/// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
/// 可以通过 _blindedBid = keccak256(value, fake, secret)
/// 设置一个秘密竞拍。
/// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。
/// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。
/// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。
/// 同一个地址可以放置多个出价。
function bid(bytes32 _blindedBid)
public
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
/// 披露你的秘密竞拍出价。
/// 对于所有正确披露的无效出价以及除最高出价以外的所有出价,你都将获得退款。
function reveal(
uint[] _values,
bool[] _fake,
bytes32[] _secret
)
public
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(_values.length == length);
require(_fake.length == length);
require(_secret.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bid = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bid.blindedBid != keccak256(value, fake, secret)) {
// 出价未能正确披露
// 不返还订金
continue;
}
refund += bid.deposit;
if (!fake && bid.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// 使发送者不可能再次认领同一笔订金
bid.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
// 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
// 返还之前的最高出价
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
/// 取回出价(当该出价已被超越)
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注)
pendingReturns[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnd()
public
onlyAfter(revealEnd)
{
require(!ended);
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}
由于区块链上面的交易都是公开透明的,前面的拍卖合约可以通过查询每一笔交易轻易得知目前的最高价。要实现盲拍确实困难。上述合约通过引入伪出价,使得真实的出价被隐藏在众多交易中,并且通过keccak256校验防止竞拍者修改自己的出价记录。实现一定程度上的盲拍。
1.转账函数
\.transfer()
address.transfer()
方法相当于 require(address.send())
, 使用transfer
方法也需要注意两点,第一点,跟send
方法一样,transfer
也只提供了2300 Energy。 第二点,不同于send
方法,transfer
方法提供了一种更安全的机制,失败的时候会抛出异常,所有已经完成的操作都会回滚。
\.send()
使用address.send()
方法需要注意两点,第一点,如上所述,它只提供了2300 Energy。 第二点,对于执行失败的send
方法,send
函数仅仅返回false
,不会抛出任何异常。因此调用send
方法的时候需要配合require
使用,否则可能会出现交易上链,用户支付了fee,但是所有的状态改动没有生效。
\.call.value()
相对于前两种方法,address.call.value(amount)( )
使用起来更加灵活,适用的范围也更加广泛。因为这种方式提供了指定energy数量的接口,使用的时候不再受2300 Energy的限制,可以允许接收函数执行更复杂的操作。使用这种方法也需要注意两点问题。第一个,同send()
一样,执行失败的时候此函数不会抛出异常,只返回false
,需要用户手动处理返回结果,使用的时候建议配合require
一起使用。第二点,如果不显示指定Energy数量,默认的Energy数量是用户所有可用的Energy。Energy数量可以通过修饰器 .gas(energyLimit)
来设定。
2.构造函数
solidity 的内置变量 now
将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。在这里,以秒为单位,因此 _biddingTime
, _revealTime
都是从当前开始经过XXX秒后
1 | constructor( |
3. constructor
构造函数是使用 constructor
关键字声明的一个可选函数, 它在创建合约时执行, 可以在其中运行合约初始化代码。如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {}
。
在执行构造函数代码之前, 如果状态变量可以初始化为指定值; 如果不初始化, 则为零。
在 0.7.0 版本之前, 你需要通过 internal
或 public
指定构造函数的可见性。
状态机
此处代码未采用书上的例子,转自一篇文章
1 | uint public value;//定义商品的价值 |
debug调试
对state的初始值有所疑问,所以进行了调试
1 | enum State { Created, Locked, Release, Inactive } |
采取remix在线部署后调试
1 | pragma solidity ^0.4.0; |
点击Debug进入调试,
简单说明
执行到最后也不会执行a函数,这是因为我们没有调用
回到部署的界面,点击一下a,会出现新的debug按钮,再点进去
继续单步执行到一个pop指令后,state
的值会改变为Locked
如果在外面直接改变state会报错。这是因为solidity语法在变量声明时只能进行一次赋值或者初始化为默认值
权限控制
本块内容其实在上面的合约中有所体现,采取白名单策略,同时编写设置白名单的函数,可以实现对用户的权限控制。
第七章
ERC20
它诞生于2015年,到2017年9月被正式标准化。协议规定了具有可互换性(fungible)代币的一组基本接口,包括代币符号、发行量、转账、授权等
接口定义
1 | contract ERC20 { |
- name:代币名字
- symbol:代币简称
- decimals:token使用小数点,
- totalSupply:token供应总量
- balanceOf:某个地址(账户)的余额
- transfer:从代币合约的调用者地址上转移_value的数量token到的地址_to,并且必须触发Transfer事件
- transferFrom:从地址_from发送数量为_value的token到地址_to,必须触发Transfer事件。transferFrom方法用于允许合同代理某人转移token。条件是from账户必须经过了approve。
- approve:允许_spender多次取回您的帐户,最高达_value金额。 如果再次调用此函数,它将以_value覆盖当前的余量。
- allowance:返回_spender仍然被允许从_owner提取的金额。
- Transfer:当成功转移token时,一定要触发Transfer事件
- Approval:当调用approval函数成功时,一定要触发Approval事件
理解后三个函数:如果账户A有1000个ETH,想允许B账户随意调用他的100个ETH
1 | 1. A账户按照以下形式调用approve函数approve(B,100) |
ERC721
ERC 721 合约标准规定了一种不可替代的代币 Non-fungible Token, NFT )的合约接 此类代币的最小单位为个,即在 ERC 20 标准中对应小数点位的 decimal 值为零
1 | contract ERC721 { |
可以看出ERC721继承了ERC20标准的一些基本功能接口。同时加入一些新的功能函数
- owner0f:根据代币ID查询该代币持有者
- tokenOfOwnerByindex:根据持有者及其索引查询所持有的代币ID
- takeOwnership:与ERC20中的transferFrom一样
- tokenMetadata:用于查看代币的元数据
ERC721代表作CryptoKitties以太坊养猫
第八章
本章主要是工具介绍和以太坊浏览器的使用
第九章
以太坊性能优化
第十章
以太坊隐私保护