Zhuang's Diary

言之有物,持之以恒

什么是稳定币

稳定币就是一种相对稳定的加密货币。这里的“稳定”主要是指价格稳定,即,在一段时间内的价格不会有大幅波动。

  1. 第一代以USDT为代表稳定币,其特征是无监管、无透明、属于自说自话的一种。

  2. 第二代以TrueUSD为代表的稳定币,比USDT进了一步,它的在透明度上要比USDT好很多,因为TUSD宣称使用托管帐户作为基金管理中使用最广泛的合法工具,为持有人提供定期审计和强有力的法律保护,即多银行负责托管帐户、第三方出具帐户余额认证、团队绝不和存入的USDT直接打交道等等,所以不会出现裁判员和运动员于一身的情况。

  3. 第三代以GUSD和PAX为代表的稳定币,则更进一步,直接以美国国家信用为背书。2018年9月10日,纽约金融服务部(NYDFS)同时批准了两种基于以太坊-ERC20发行的稳定币,分别是 Gemini 公司发行的稳定币Gemini Dollar(GUSD),与Paxos公司发行的稳定币Paxos。Standard (PAX),每个稳定币都有 1 美元支撑。此外,还有两个非常突出的特点:一个是获得政府部门纽约金融服务部正式批准,成为第一个合规合法、接受监督的稳定币(也就意味着受到法律保护),信用背书大幅提升;另一个是基于以太坊的ERC20来发行的,这意味着财务相关数据完全公开透明、不可窜改,而且完全去中心化。那么理论上说,每一笔GUSD的增发都会有相应的资金入账。和完全中心化的稳定币对比,对于投资者来说,无疑更加具有可信度。

但我们也应该看到,这次给GUSD颁发执照的,是NYDFS(纽约州金融服务部门),即一个地方金融部门。美国州级金融部门的监管并不意味着在全美有效,NYDFS(纽约州金融服务部门)上面还有SEC和美联储,这两个部门的态度才是决定稳定币未来的关键。而且GUSD目前只有25万美金上限的保额(GUSD发行的美元是存放在美国的在岸银行账户上的,这个账户适用FDIC的存款保险)。所以此次GUSD作为试点的目的很浓,就像我们国家一样,新东西慢慢来,先搞个试点,从中总结经验、完善相关法律法规,待成熟后逐步全面推广。

关于ERC20代币

如果问以太坊的智能合约应用最广泛的地方在哪里, 肯定就是发币了。 虽然可能V神也没想到世界还没被改变, ICO(圈钱)的方式倒是又多出了一个。

既然是数字token, 代码实现起来就可以有各种各样的方案。 实质无外乎就是记录下每个用户拥有的数字token数量, 具有转账,查询余额等功能就行了。 上面我们说到只要调用对应相关的函数去执行即可完成相关的功能。 可是这个时候假如某个ICO方A 写了代币转账功能, 它的转账函数叫做tx(uint256). 那么如果想调用它的转账就要调用tx这个函数, 我们知道只要调用函数不相同, input的内容就不会一样。 如果所有的ICO方写的这些函数名称均不一样。 调用者就要查看每一个ICO方的合约代码。 于是这个时候ERC20就来了, 它定义了一些发币(圈钱)的规范。也即是如果你想发行ICO, 最好按着我的规范来, 这样大家用起来就跟方便了。ERC20简而言之就是定义了下面几个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract ERC20 {

uint256 public totalSupply;

event Transfer(address indexed from, address indexed to, uint256 value); // 转账触发
event Approval(address indexed owner, address indexed spender, uint256 value); // 容许提取触发

function balanceOf(address who) public view returns (uint256); // 查询余额函数
function transfer(address to, uint256 value) public returns (bool); // 进行转账函数

function approve(address spender, uint256 value) public returns (bool); // 容许某个地址提款
function transferFrom(address from, address to, uint256 value) public returns (bool); // 从一方向另一方转账的余额

}

有了这个规范, 各个ICO方在发行token时都实现上面的接口, 这样无论任何的ERC20代币, 均可以用一套方法实现所有代币转账,查询余额等功能。

分析一下PAX稳定币的智能合约代码

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
pragma solidity ^0.4.24;
pragma experimental "v0.5.0";

// 导入外部包 此包中主要是一些
// 安全的数学运算
// 因为在一些场景 出现了数据溢出没有考虑的问题导致了
// 一些ico币直接归零
import "./zeppelin/SafeMath.sol";

contract PAXImplementation {

using SafeMath for uint256;
bool private initialized = false;

// 定义了ERC20规定的代币名称 符号 精度
mapping(address => uint256) internal balances;
uint256 internal totalSupply_;
string public constant name = "PAX"; // solium-disable-line uppercase
string public constant symbol = "PAX"; // solium-disable-line uppercase
uint8 public constant decimals = 18; // solium-disable-line uppercase

// ERC20 DATA
mapping (address => mapping (address => uint256)) internal allowed;

// OWNER DATA
address public owner;

// PAUSABILITY DATA
bool public paused = false;

// LAW ENFORCEMENT DATA
address public lawEnforcementRole;
mapping(address => bool) internal frozen;

// SUPPLY CONTROL DATA
address public supplyController;

// 定义触发时间 当转账或者授权别人转账时 调用此事件 当调用时 其实质会在以太坊节点区块上写入日志。
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);

// OWNABLE EVENTS
event OwnershipTransferred(
address indexed oldOwner,
address indexed newOwner
);

// PAUSABLE EVENTS
event Pause();
event Unpause();

// LAW ENFORCEMENT EVENTS
event AddressFrozen(address indexed addr);
event AddressUnfrozen(address indexed addr);
event FrozenAddressWiped(address indexed addr);
event LawEnforcementRoleSet (
address indexed oldLawEnforcementRole,
address indexed newLawEnforcementRole
);

// SUPPLY CONTROL EVENTS
event SupplyIncreased(address indexed to, uint256 value);
event SupplyDecreased(address indexed from, uint256 value);
event SupplyControllerSet(
address indexed oldSupplyController,
address indexed newSupplyController
);

/**
* FUNCTIONALITY
*/

// INITIALIZATION FUNCTIONALITY

/**
合约部署时的初始化过程
设置合约拥有者为部署合约的账户
设置总供应量为0
并保证此函数只会被调用一次
*/
function initialize() public {
require(!initialized, "already initialized");
owner = msg.sender;
lawEnforcementRole = address(0);
totalSupply_ = 0;
supplyController = msg.sender;
initialized = true;
}

/**
合约的构造函数 调用上面的初始化函数 并且设置暂停交易
*/
constructor() public {
initialize();
pause();
}

// ERC20 BASIC FUNCTIONALITY

/**
ERC20接口 返回总的供应量
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}

/*
转账函数 实现将调用者的token转给其他人
msg.sender 即为合约的调用者
并且此函数要求必须是非暂停状态 即whenNotPaused返回真

这个函数有需要验证条件
1.交易没有被暂停
2.接收方地址不能是0
3.接收方和发起方均不可以是冻结地址
4.转账的token余额要足够。
*/
function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) {
require(_to != address(0), "cannot transfer to address zero");
require(!frozen[_to] && !frozen[msg.sender], "address frozen");
require(_value <= balances[msg.sender], "insufficient funds");

balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}

/**
ERC20接口 返回某个账户的token余额
*/
function balanceOf(address _addr) public view returns (uint256) {
return balances[_addr];
}

// ERC20 FUNCTIONALITY

/*
ERC20接口 实现了 _from地址下容许调用方可以转出金额到其他_to
此函数要求必须是非暂停状态
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
whenNotPaused
returns (bool)
{
require(_to != address(0), "cannot transfer to address zero");
require(!frozen[_to] && !frozen[_from] && !frozen[msg.sender], "address frozen");
require(_value <= balances[_from], "insufficient funds");
require(_value <= allowed[_from][msg.sender], "insufficient allowance");

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}

/**
ERC20接口 实现调用方容许_spender 可以从我的账户转出的金额 这个函数和上面的函数是相对应的。
只有一个账户容许了其他账户能从我的账户转出的金额 上述的函数才能转账成功。
*/
function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) {
require(!frozen[_spender] && !frozen[msg.sender], "address frozen");
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

/**
ERC20接口 返回_owner账户容许_spender账户从自己名下转移出去的资产数量
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}

// OWNER FUNCTIONALITY

/**
这个函数被称为修饰函数 上面的whenNotPaused 也是一个修饰函数 实质是一种断言。 只有
断言通过 才会执行函数内部的内容
*/
modifier onlyOwner() {
require(msg.sender == owner, "onlyOwner");
_;
}

/*
将只能合约的拥有者转给别人
*/
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0), "cannot transfer ownership to address zero");
emit OwnershipTransferred(owner, _newOwner);
owner = _newOwner;
}

// PAUSABILITY FUNCTIONALITY

/**
* 修饰函数 要求处于非暂停交易状态
*/
modifier whenNotPaused() {
require(!paused, "whenNotPaused");
_;
}

/**
* 只有合约的拥有者才可以设置暂停交易
*/
function pause() public onlyOwner {
require(!paused, "already paused");
paused = true;
emit Pause();
}

/**
* 只有合约的拥有者才能取消暂停交易
*/
function unpause() public onlyOwner {
require(paused, "already unpaused");
paused = false;
emit Unpause();
}

// LAW ENFORCEMENT FUNCTIONALITY

/**
设置一个法定的的强制角色 这个角色可以冻结或者解冻别人账户的token
设置一个这样的角色要求首先调用方要么是合约的拥有者 要么自己已经是法定的强制者
* @param _newLawEnforcementRole The new address allowed to freeze/unfreeze addresses and seize their tokens.
*/
function setLawEnforcementRole(address _newLawEnforcementRole) public {
require(msg.sender == lawEnforcementRole || msg.sender == owner, "only lawEnforcementRole or Owner");
emit LawEnforcementRoleSet(lawEnforcementRole, _newLawEnforcementRole);
lawEnforcementRole = _newLawEnforcementRole;
}

// 断言函数 要求调用方必须是强制者角色
modifier onlyLawEnforcementRole() {
require(msg.sender == lawEnforcementRole, "onlyLawEnforcementRole");
_;
}

/**
冻结某个账户的token 使用了断言 onlyLawEnforcementRole 也是只有调用方角色是
法定强制者才有权限冻结别人的token
*/
function freeze(address _addr) public onlyLawEnforcementRole {
require(!frozen[_addr], "address already frozen");
frozen[_addr] = true;
emit AddressFrozen(_addr);
}

/**
解冻某个账户的token 使用了断言 onlyLawEnforcementRole 也是只有调用方角色是
法定强制者才有权限解冻别人的token
*/
function unfreeze(address _addr) public onlyLawEnforcementRole {
require(frozen[_addr], "address already unfrozen");
frozen[_addr] = false;
emit AddressUnfrozen(_addr);
}

/**
摧毁冻结账户的token 也就是说如果这个地址是一个冻结地址调用这个函数会把这个地址的token销毁同时总供应数量也会被减少
当然这个函数也不是谁都可以调用的 只有法定的强制者才有权限
*/
function wipeFrozenAddress(address _addr) public onlyLawEnforcementRole {
require(frozen[_addr], "address is not frozen");
uint256 _balance = balances[_addr];
balances[_addr] = 0;
totalSupply_ = totalSupply_.sub(_balance);
emit FrozenAddressWiped(_addr);
emit SupplyDecreased(_addr, _balance);
emit Transfer(_addr, address(0), _balance);
}

/**
用于检查某个地址是否被冻结了
*/
function isFrozen(address _addr) public view returns (bool) {
return frozen[_addr];
}

// SUPPLY CONTROL FUNCTIONALITY

/**
设置token供应量的控制着 在合约初始化时 token供应量是合约发起则 调用这个函数可以更改
这个函数只有调用方已经是token供应量控制着或者整个合约的拥有者才能调用成功
也就是在整个合约中 合约的拥有者实质是可以控制一切权限的。 它能更改法定强制者 更改总token
供应量的控制者。
*/
function setSupplyController(address _newSupplyController) public {
require(msg.sender == supplyController || msg.sender == owner, "only SupplyController or Owner");
require(_newSupplyController != address(0), "cannot set supply controller to address zero");
emit SupplyControllerSet(supplyController, _newSupplyController);
supplyController = _newSupplyController;
}

modifier onlySupplyController() {
require(msg.sender == supplyController, "onlySupplyController");
_;
}

/**
增加总的token供应量 并把新增供应量加到supplyController这个账户的名下。
*/
function increaseSupply(uint256 _value) public onlySupplyController returns (bool success) {
totalSupply_ = totalSupply_.add(_value);
balances[supplyController] = balances[supplyController].add(_value);
emit SupplyIncreased(supplyController, _value);
emit Transfer(address(0), supplyController, _value);
return true;
}

/**
减少总的token供应量 待减少供应量从supplyController这个账户的名下减掉 。
这个函数要求supplyController
*/
function decreaseSupply(uint256 _value) public onlySupplyController returns (bool success) {
require(_value <= balances[supplyController], "not enough supply");
balances[supplyController] = balances[supplyController].sub(_value);
totalSupply_ = totalSupply_.sub(_value);
emit SupplyDecreased(supplyController, _value);
emit Transfer(supplyController, address(0), _value);
return true;
}
}

PAX稳定币功能概述

PAX除了具有这个ERC20的功能外,还具有一些其他功能:

  1. 可以暂停整个代币转账
  2. 可以增加或者减少整个代币的数量
  3. 可以任意冻结或者解冻某个账户的代币
  4. 可以销毁某个冻结账户的代币
  5. 可以转移合约控制权。可以转移总供应量控制权。

总的来说PAX币做的限制特别多, 它的合约拥有机会可以做任何事情。 就算token转移给你了, 依然能分分钟钟消失。

ethereum的虚拟机源码所有部分在core/vm下。 去除测试总共有24个源码文件。 整个vm调用的入口在go-ethereum/core/state_transaction.go中。 我们主要是为了分析虚拟机源码,所以关于以太坊是如何进行交易转账忽略过去。

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
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
contractCreation := msg.To() == nil

// Pay intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}

var (
evm = st.evm
// vm errors do not effect consensus and are therefor
// not assigned to err, except for insufficient balance
// error.
vmerr error
)
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
.........

从上面的截图我们可以看出, 当以太坊的交易中to地址为nil时, 意味着部署合约, 那么就会调用evm.Create方法。 否则调用了evm.Call方法。 也就是说分析以太坊虚拟机源码时, 只要从这两个函数作为入口即可。

首先我们先看一下EVM数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type EVM struct {
// Context provides auxiliary blockchain related information
Context
// StateDB是状态存储接口。 这个接口非常重要。 可以肯定的说evm中的大部分工作都是围绕这次接口进行的。
StateDB StateDB
// 记录当前调用的深度
depth int
// 记录链的配置 主要是以太坊经理过几次分叉和提案 为了兼容之前的区块信息
// 所以做了一些兼容 移植的时候我们只考虑最新版本的内容
chainConfig *params.ChainConfig
// 这个参数 对我们移植过程中的意义不是很大
chainRules params.Rules
// 这个是虚拟机的一些配置参数 是创建解释器的初始化参数 比如所有操作码对应的函数也是在此处配置的
vmConfig Config
// 解释器对象 它是整个进行虚拟机代码执行的地方。
interpreter *Interpreter
// 用来终止代码执行
abort int32
// callGasTemp holds the gas available for the current call. This is needed because the
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
}

先看一看创建EVM的方法

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
func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM {
evm := &EVM{
Context: ctx,
StateDB: statedb,
vmConfig: vmConfig,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(ctx.BlockNumber),
}

// 主要看这个地方 创建解释器 解释器是执行字节码的关键
evm.interpreter = NewInterpreter(evm, vmConfig)
return evm
}

func NewInterpreter(evm *EVM, cfg Config) *Interpreter {
// 在这里设置 操作码对应的函数
// 主要原因是以太坊经历版本迭代之后 操作码有了一些变化
// 我们移植的时候 这个地方只会保留最新版本的操作码表
if !cfg.JumpTable[STOP].valid {
switch {
case evm.ChainConfig().IsConstantinople(evm.BlockNumber):
cfg.JumpTable = constantinopleInstructionSet
case evm.ChainConfig().IsByzantium(evm.BlockNumber):
cfg.JumpTable = byzantiumInstructionSet
case evm.ChainConfig().IsHomestead(evm.BlockNumber):
cfg.JumpTable = homesteadInstructionSet
default:
cfg.JumpTable = frontierInstructionSet
}
}

return &Interpreter{
evm: evm,
cfg: cfg,
// gas表中记录了对应的执行操作需要花费的gas 移植的时候我们只保留一个
gasTable: evm.ChainConfig().GasTable(evm.BlockNumber),
}
}

接下来我们先分析部署合约的入口, 看一看整个合约部署的流程。

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
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {

// 首先检测当前evm执行的深度 默认不应该超过1024
if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{}, gas, ErrDepth
}
// 这个函数我们不在追踪 其功能就是检测是否调用方的金额大约value
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, common.Address{}, gas, ErrInsufficientBalance
}
// 首先获取调用者的nonce 然后再更新调用者的nonce 这个如果熟悉以太坊交易流程的话应该知道nonce的作用。
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)

// 下面这三句就是根据调用者地址 调用者nonce创建合约账户地址 并且判断是否这个合约地址确实没有部署过合约
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
contractHash := evm.StateDB.GetCodeHash(contractAddr)
if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
return nil, common.Address{}, 0, ErrContractAddressCollision
}
// 既然已经创建好了合约地址 那么就要为这个合约地址创建账户体系
snapshot := evm.StateDB.Snapshot()
// 所以下面这个函数在一直的时候我们的工作内容之一
evm.StateDB.CreateAccount(contractAddr)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
evm.StateDB.SetNonce(contractAddr, 1)
}
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)

// 创建一个合约对象 设置合约对象的参数 比如调用者 合约代码 合约hash的内容
contract := NewContract(caller, AccountRef(contractAddr), value, gas)
contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)

if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, contractAddr, gas, nil
}

if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, value)
}
start := time.Now()

// 将evm对象 合约对象传入run函数开始执行 此函数是核心 等一会分析到Call入口的时候最终也会调用此函数
ret, err = run(evm, contract, nil)

// 上述函数执行完成后返回的就是我前一章所说的初始化后的合约代码
// 也就是我们在remix上看到runtime的字节码 以后调用合约代码其实质就是
// 执行返回后的代码

// 下面的流程主要是一些检查 把返回的字节码保存到此合约账户名下 这样以后调用合约代码才能加载成功
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
// if the contract creation ran successfully and no errors were returned
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
if err == nil && !maxCodeSizeExceeded {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(contractAddr, ret)
} else {
err = ErrCodeStoreOutOfGas
}
}

// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
// Assign err if contract code size exceeds the max while the err is still empty.
if maxCodeSizeExceeded && err == nil {
err = errMaxCodeSizeExceeded
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}
return ret, contractAddr, contract.Gas, err
}

所以我们下面就开始主要分析run函数

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
func run(evm *EVM, contract *Contract, input []byte) ([]byte, error) {
if contract.CodeAddr != nil {
// 首先会进入下面这个代码执行 它会根据给定的合约地址来判断是否是以太坊内部已经保存的合约
// 如果是创建新合约 肯定不是内置合约
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if p := precompiles[*contract.CodeAddr]; p != nil {
return RunPrecompiledContract(p, input, contract)
}
}
// 所以最后我们最终到这里 此时input参数为nil
return evm.interpreter.Run(contract, input)
}

// 解释器的Run函数才是真正执行合约代码的地方
// 为了凸显主流程 我们隐藏部分内容
func (in *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err error) {
if in.intPool == nil {
in.intPool = poolOfIntPools.get()
defer func() {
poolOfIntPools.put(in.intPool)
in.intPool = nil
}()
}

// 下面这些变量应该说满足了一个字节码执行的所有条件
// 有操作码 内存 栈 PC计数器
// 强烈建议使用debug工具去跟踪一遍执行的流程
// 其实它的执行流程就和上一章我们人肉执行的流程一样
var (
op OpCode // current opcode
mem = NewMemory() // bound memory
stack = newstack() // local stack
pc = uint64(0) // program counter
cost uint64
pcCopy uint64 // needed for the deferred Tracer
gasCopy uint64 // for Tracer to log gas remaining before execution
logged bool // deferred Tracer should ignore already logged steps
)
contract.Input = input

// Reclaim the stack as an int pool when the execution stops
defer func() { in.intPool.put(stack.data...) }()

// 开始循环PC计数执行 直到有中止执行或者跳出循环
for atomic.LoadInt32(&in.evm.abort) == 0 {
// 根据PC计数器获取操作码
op = contract.GetOp(pc)
// 根据操作码获取对应的操作函数
operation := in.cfg.JumpTable[op]

// 验证栈中的数据是否符合操作码需要的数据
if err := operation.validateStack(stack); err != nil {
return nil, err
}
// If the operation is valid, enforce and write restrictions
if err := in.enforceRestrictions(op, operation, stack); err != nil {
return nil, err
}

var memorySize uint64
// 有些指令是需要额外的内存消耗 在jump_table.go文件中可以看到他们具体每个操作码的对应的额外内存消耗计算
// 并不是所有的指令都需要计算消耗的内存
// memorySize指向对应的计算消耗内存的函数 根据消耗的内存来计算消费的gas
if operation.memorySize != nil {
memSize, overflow := bigUint64(operation.memorySize(stack))
if overflow {
return nil, errGasUintOverflow
}
if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
return nil, errGasUintOverflow
}
}
// 计算此操作花费的gas数量
cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
if err != nil || !contract.UseGas(cost) {
return nil, ErrOutOfGas
}
if memorySize > 0 {
mem.Resize(memorySize)
}

// 开始执行此操作码对应的操作函数 同时会返回执行结果同时也会更新PC计数器
// 大部分的操作码对应的操作函数都是在instructions.go中可以找得到
res, err := operation.execute(&pc, in.evm, contract, mem, stack)
// 如果这个操作码是一个返回参数 那么就把需要的内容写入returnData
// 按理说应该是只有return参数才会有范湖
if operation.returns {
in.returnData = res
}

// 到这里也就意味着一个操作码已经执行完成了 应该根据这次的执行结果来决定下一步的动作
// 1. 如果执行出错了 直接返回错误
// 2. 如果只能合约代码中止了(比如断言失败) 那么直接返回执行结果
// 3. 如果是暂停指令 则直接返回结果
// 4. 如果操作符不是一个跳转 则直接PC指向下一个指令 继续循环执行
switch {
case err != nil:
return nil, err
case operation.reverts:
return res, errExecutionReverted
case operation.halts:
return res, nil
case !operation.jumps:
pc++
}
}
return nil, nil
}

到了这里整个部署合约流程就完成了, 部署合约时是从evm.Create->run->interper.run 然后在执行codeCopy指令后把runtime的内容返回出来。 在evm.Create函数中我们也看到了当run执行完成后会把runtime的合约代码最终设置到合约地址名下。 整个合约部署就算完成了。

分析完合约创建接着就该分析合约调用代码了。 调用智能合约和部署在以太坊交易上看来就是to的地址不在是nil而是一个具体的合约地址了。 同时input的内容不再是整个合约编译后的字节码了而是调用函数和对应的实参组合的内容。 这里就涉及到另一个东西那就是abi的概念。此处我不打算详细说明, abi描述了整个接口的详细信息, 根据abi可以解包和打包input调用的数据。

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
// 忽略一些隐藏了主线的内容
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {

var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
// 首先要判断这个合约地址是否存在 如果不存在是否是内置的合约
if !evm.StateDB.Exist(addr) {
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
// Calling a non existing account, don't do antything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
}
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
// 执行进行以太币的转账过程
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

// 是不是很熟悉 不管是部署合约还是调用合约都要先创建合约对象 把合约加载出来挂到合约对象下
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

start := time.Now()

// 依然是调用run函数来执行代码 不同之处在于这次的input不在是nil了 而是交易的input内容
// 在上一节中我们看到CALLDATALOAD这个指令会用到input的内容
ret, err = run(evm, contract, input)

if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
// 最终返回执行结果
return ret, contract.Gas, err
}

应该说到了这里只能合约流程就完事了, 可是也许你会好奇命名evm里面有那么多的内容没有分析到。 但是整个流程确实就是这些。 其他的比如栈对象是如何模拟的, 内存是如何模拟的。 操作码对应的操作函数,及其相关gas花费怎么计算的都没有说明。 可是我觉得首先知道整个流程和原理。阅读这些就比较容易了, 因为我们知道目的和原理, 就会明白它的那些代码的作用了。 如果我上去就说那些东西, 整个主线就会被淹没了。

最后还有一个比较重要的接口要说明一下, 它是我们接下来移植中要重点修改的内容。

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
type StateDB interface {
// 创建账户函数 表明evm需要你执行创建一个新的账户体系
CreateAccount(common.Address)

// 减去一个账户的余额
SubBalance(common.Address, *big.Int)
// 添加一个账户的余额
AddBalance(common.Address, *big.Int)
// 获取一个账户的余额
GetBalance(common.Address) *big.Int
// 获取账户的nonce 因为以太坊要根据nonce在决定交易的执行顺序和合约地址的生成
GetNonce(common.Address) uint64
// 更新合约的nonce
SetNonce(common.Address, uint64)

// 获取合约地址的整个合约代码的hash值
GetCodeHash(common.Address) common.Hash
// 获取合约代码
GetCode(common.Address) []byte
// 设置合约代码
SetCode(common.Address, []byte)
// 获取合约代码的大小
GetCodeSize(common.Address) int
// 获取和添加偿还金额
AddRefund(uint64)
GetRefund() uint64
// 注意这两个函数很重要 其实质就是相当于数据库的select和update
// 一个智能合约的全局静态数据的读取和写入就是通过这两个函数
GetState(common.Address, common.Hash) common.Hash
SetState(common.Address, common.Hash, common.Hash)

// 合约账户自杀 或者是否已经自杀 主要是以太坊的一个机制 自杀的合约会给与退费
Suicide(common.Address) bool
HasSuicided(common.Address) bool

// 判断一个合约是否存在
Exist(common.Address) bool
// 判断合约是否为空
// is defined according to EIP161 (balance = nonce = code = 0).
Empty(common.Address) bool

RevertToSnapshot(int)
Snapshot() int

// 此函数就是在我们在智能合约中执行emit命令时调用的
AddLog(*types.Log)
AddPreimage(common.Hash, []byte)

// 这个接口在evm中没有使用到 我们可以写一个空函数
ForEachStorage(common.Address, func(common.Hash, common.Hash) bool)
}

到此整个evm分析就结束了,

什么是OCR?

OCR(Optical Character Recognition,光学字符识别),针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,并通过识别软件将图像中的文字转换成文本格式,供文字处理软件进一步编辑加工的技术。

什么是tesseract?

谈到OCR就一定要首先聊聊tesseracttesseract的OCR引擎。最先由HP实验室于1985年开始研发,至1995年时已经成为OCR业内最准确的三款识别引擎之一。然而,HP不久便决定放弃OCR业务,Tesseract也从此尘封。

数年以后,HP意识到,与其将Tesseract束之高阁,不如贡献给开源软件业,让其重焕新生--2005年,Tesseract由美国内华达州信息技术研究所获得,并求诸于Google对Tesseract进行改进、消除Bug、优化工作。目前已作为开源项目发布,大部分的开发人员均来自于Google。

目前最新版本是4.0(Oct 29 2018)。4.0版本主要增加了新的基于LSTM神经网络的引擎,识别率和正确率得到提高。

  • 添加了一个新的OCR引擎,该引擎使用基于LSTM的神经网络系统,具有很高的准确性。
  • 这包括LSTM OCR引擎的新的训练工具。可以从头开始或通过微调现有模型来训练新模型。
  • 添加了经过训练的数据,其中包括多种语言的LSTM模型。

tesseract的相关库

Python-tesseract是tesseract-OCR引擎的Python包装器。 它作为独立的调用脚本也很有用,因为它可以读取Python Imaging Library支持的所有图像类型,包括jpeg,png,gif,bmp,tiff等,而tesseract-ocr默认只支持 tiff 和 bmp。此外,如果用作脚本,Python-tesseract也可以将已识别的文本直接打印出来。

tesseract.js是tesseract-OCR引擎的JavaScript库器。通过npm安装

1
npm install tesseract.js --save

tesseract的相应软件

gImageReader:A Gtk/Qt front-end to tesseract-ocr.

TesseractStudio.Net:A free Windows graphical interface to the Tesseract 4.0 OCR engine.

待续其他:

  • tesseract中LSTM的具体分析;
  • tesseract如何使用;
  • pytesseract如何使用;
  • tesseract使用的正确率测试

关于tesseract的训练介绍

Get a fatal error: runtime: out of memory in geth. Env is as below:

  • geth version: 1.8.15-stable
  • go version: 1.11 linux/amd64
  • Linux version 4.14.47-64.38.amzn2.x86_64
  • (mockbuild@ip-10-0-1-219)
  • (gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC))
  • aws型号 c5.large | vCPU:2 | 内存 (GiB):4
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
INFO [11-22|01:36:30.725] Submitted transaction                    fullhash=0x583c48c36a165daed190212facf12813b2a5331b05e43ede7a1e8426936d83d6 recipient=0xe2E55079E31cf95589B5433cF06beb448624d0A7
INFO [11-22|01:36:31.286] Committing block stateHash=cc2f6d…6fbe5b blockHash=3b92a3…2a3b13
INFO [11-22|01:36:31.287] Imported new chain segment blocks=1 txs=1 mgas=0.021 elapsed=428.986µs mgasps=48.953 number=3 hash=607b71…154d5e cache=964.00B
INFO [11-22|01:36:31.287] Commiting module=oval blockHash=3b92a30f0a4b332a95b5bf304d133b5ebe76210f6c329b14148069a77c2a3b13 cbr=cc2f6d729a06198723f407523b4ac61796a0198aa9e9e3d5a1c17e37e56fbe5b
INFO [11-22|01:42:00.336] Committing block stateHash=de1af6…3b83f8 blockHash=71c97f…2c1bdc
INFO [11-22|01:42:00.337] Imported new chain segment blocks=1 txs=1 mgas=0.075 elapsed=456.862µs mgasps=163.472 number=4 hash=295456…55f08d cache=1.27kB
INFO [11-22|01:42:00.337] Commiting module=oval blockHash=71c97f9a55e28a03d9c114a9749d6a8f50dccbc2a3538f4ce6818fb2c52c1bdc cbr=de1af60245d823ff58d280ced344046ef4348957b83900d55cfd8e108d3b83f8
fatal error: runtime: out of memory

runtime stack:
runtime.throw(0x10bd3c9, 0x16)
/usr/local/go/src/runtime/panic.go:608 +0x72
runtime.sysMap(0xc0cc000000, 0x10000000, 0x217fdf8)
/usr/local/go/src/runtime/mem_linux.go:156 +0xc7
runtime.(*mheap).sysAlloc(0x2165e60, 0x10000000, 0x313d68, 0x7fcb9139acd8)
/usr/local/go/src/runtime/malloc.go:619 +0x1c7
runtime.(*mheap).grow(0x2165e60, 0x8000, 0x0)
/usr/local/go/src/runtime/mheap.go:920 +0x42
runtime.(*mheap).allocSpanLocked(0x2165e60, 0x8000, 0x217fe08, 0x448fab)
/usr/local/go/src/runtime/mheap.go:848 +0x337
runtime.(*mheap).alloc_m(0x2165e60, 0x8000, 0x7fcb9cf00101, 0x7fcb9cf025a0)
/usr/local/go/src/runtime/mheap.go:692 +0x119
runtime.(*mheap).alloc.func1()
/usr/local/go/src/runtime/mheap.go:759 +0x4c
runtime.(*mheap).alloc(0x2165e60, 0x8000, 0x7fcb91010101, 0x42baf5)
/usr/local/go/src/runtime/mheap.go:758 +0x8a
runtime.largeAlloc(0x10000000, 0x460101, 0x7fcb9d0896c0)
/usr/local/go/src/runtime/malloc.go:1019 +0x97
runtime.mallocgc.func1()
/usr/local/go/src/runtime/malloc.go:914 +0x46
runtime.systemstack(0xc000308480)
/usr/local/go/src/runtime/asm_amd64.s:351 +0x66
runtime.mstart()
/usr/local/go/src/runtime/proc.go:1229
......

【原因分析】
geth莫名其妙自动关闭,日志未呈现异常。此问题是因为服务器内存不足触发Linux的OOM killer操作,被杀掉了。
【解决方案】

  1. 设置 –cache,并不能解决问题。
  2. 除了升级内存,没有太好的办法。只能依靠监控程序报警,发现问题重启即可。
  3. 需要在节点间设置 rpc 通信的白名单,防止黑客暴力破解 keystore 的密码。
  4. 其中折中的办法是设置swap。具体命令如下:
1
2
3
4
5
6
7
8
free -m
dd if=/dev/zero of=/swap bs=10M count=800
chmod 600 /swap
ll /swap
mkswap /swap
swapon /swap
swapon -s
free -m

相关链接: