尚硅谷以太坊区块链学习
Posted 难、起名
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了尚硅谷以太坊区块链学习相关的知识,希望对你有一定的参考价值。
尚硅谷以太坊区块链学习(3)
前言
提示:服务外包区块链学习
只记录操作
尚硅谷以太坊区块链直达链接
一、搭建私链
1、创建创世区块
创建目录
$ mkdir myChain
$ cd myChain
创建名为genesis.json的文件,并输入内容
$ touch genesis.json
$ vi genesis.json
尚硅谷创世区块内容:
"config":
"chainId": 15
,
"difficulty": "2000",
"gasLimit": "2100000",
"alloc":
"0x634c4CF26680fa8C2Ef3d8B9FA4E6AE748667BB8": "balance": "300000000000000000000"
根据尚硅谷的创世区块配置的私有链后面发起交易的时候会报交易未初始化的错误,于是在网上找到不报错的创世区块配置如下,具体项是干嘛的我也不太懂,想了解请自行百度
"config":
"chainId": 666,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"ethash":
,
"nonce": "0x0",
"timestamp": "0x5ddf8f3e",
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x47b760",
"difficulty": "0x00002",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": "0x634c4CF26680fa8C2Ef3d8B9FA4E6AE748667BB8":"balance":"300000000000000000000" ,
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
2、区块链初始化
初始化命令:
$ geth --datadir . init genesis.json
结果
3、正式启动私有链
$ geth --datadir . --networkid 666 console
networkid 区块链id 必须跟创世区块里的id一致
console 打印日志
错误
这样启动就会一直在找寻节点一直报错
不报错命令
geth --datadir . --networkid 15 --nodiscover --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" --port 30305 --allow-insecure-unlock console 2>>geth.log
nodiscover 可以不让它一直找寻节点
http,http.addr,http.vhosts,http.port,http.corsdomain 都是跟本地服务有关,后面会讲是用来干嘛的
console 2>>geth.log将输出日志打印到geth.log,方便查看
打开另一个终端,输入以下命令即可看打印的日志
tail -f geth.log
二、Geth控制台操作
1、常见对象与常用命令
常见对象
eth:主要包括对区块链进行访问和交互相关的方法;
net:主要包括查看P2P网络状态的方法;
admin:主要包括与管理节点相关的方法;
miner:主要包括挖矿相关的方法;
personal:包括账户管理的方法;
txpool:包括查看交易内存池的方法;
web3:包括以上所有对象,还包括一些通用方法。
常见命令
personal.newAccount():创建账户;
personal.unlockAccount():解锁账户;
eth.accounts:列出系统中的账户;
eth.getBalance():查看账户余额,返回值的单位是Wei;
eth.blockNumber():列出当前区块高度,也就是区块数量;
eth.getTransaction():获取交易信息;
eth.getBlock():获取区块信息;
miner.start():开始挖矿;
miner.stop():停止挖矿;
web3.fromWei():Wei换算成以太币;
web3.toWei():以太币换算成Wei;
txpool.status:交易池中的状态。
2、实际操作
1、查看预设帐号
创世区块中alloc就是预设的帐号,balance就是预设帐号的以太币余额
eth.getBalance("0x634c4CF26680fa8C2Ef3d8B9FA4E6AE748667BB8")
Wei到Ether的转化
web3.fromWei(eth.getBalance("0x634c4CF26680fa8C2Ef3d8B9FA4E6AE748667BB8"))
2、建立新帐号
两种方法
方法后输入密码
personal.newAccount()
方法参数填密码
personal.newAccount("1234")
这个时候再查看一下帐号
eth.accounts
就会有当前链上所有的帐号
注册过的帐号都会在这里,但是预设帐号不会在
3、开始挖矿
默认第一个注册的帐号为挖矿帐号,所以涨币也是会给第一个帐号
miner.start(1)
注意观察日志
这就说明在准备挖矿了,安心等待
出现小锤子就表示挖矿成功了
结束挖矿
miner.stop()
查看链的区块高度,即区块总数
eth.blockNumber
再查看第一个注册帐号的余额
eth.getBalance(eth.accounts[0])
3、发起交易
交易前要把帐号解锁
personal.unlockAccount(eth.accounts[0])
personal.unlockAccount(eth.accounts[1])
发起交易
eth.sendTransaction(from:eth.accounts[0],to:eth.accounts[1],value:100000000000000000)
结果会返回交易的Hash值
使用以下命令可以查看交易池
txpool
交易需要打包才会生效
所以又需要挖矿
挖矿成功之后就可以查看余额是否发生了变化
三、链接MataMask
1、链接MataMask
选择Localhost 8545
这个时候明白启动私链时有关的网络配置的原因了
链接成功后原始帐号就会是创世区块中的预设帐号以及它的余额
这个时候可以选择导入帐号
选择Json文件
到链文件目录下keystore下选择要导入的帐号文件
再输入注册帐号时输入的密码就可以了
等待导入时间可能会很长
设置->网络->Localhost 8545
修改链ID为创世区块中的chainId
保存
这样可以保证后面用MataMask发交易尽可能不出错
到这的时候就已经可以用MataMask在你的帐号之间转币了
不过发起交易之后也需要在Geth客户端对交易进行打包
打包成功之后MataMask会给提示
Over
区块链学习笔记之以太坊
二、以太坊
9. 以太坊(ETH)智能合约
本章节我们来介绍一下智能合约,智能合约是以太坊的精髓。
9.1 智能合约简介
(1)智能合约的本质:运行在区块链系统上的一段代码,这个代码的逻辑就定义了合约内容。智能合约的账户保存了合约当前的运行状态,合约的状态包括以下几项:
- balance:当前余额
- nonce:交易次数
- code:合约代码
- storage:存储,数据结构为一棵MPT
(2)Solidity是智能合约最常用的语言,其语法与JavaScript很接近。下图显示了智能合约的代码结构。
- 第一行声明一下所使用的Solidity的版本号不同版本的Solidity在语法上有一些小的差别。Solidity是面向对象的编程语言,这里的contract类似于c++中的类(class)。
- 这里的contract定义了很多状态变量。Solidity是强类型语言(strongly typed),这里的变量类型大部分和c++之类的是比较接近的,比如uint类型是unsigned int,也就是无符号整数。address类型是Solidity语言所特有的。后面会细说一下地址类型的成员变量和成员函数。
- 接下来是两个event(事件)。这个事件的作用是用来记录日志的,也就是说用来打log。第一个事件是HighestBidIncrease(),即拍卖的最高出价增加了。声明一下,我们所使用的代码示例是网上拍卖的一个例子,拍卖的具体规则,后面细说。
- 让我们接着看这个事件,如果某个人出一个新的最高价,那么我们就记录一下参数bidder(投标人的地址),金额是amount。第二个事件是Pay2Beneficiary(),它的参数是赢得拍卖人的地址winner以及他最后的出价amount。
- Solidity语言和普通的编程语言相比,它有一些特别之处,比如下图中的mapping,我们可以看出mapping是一个哈希表,保存了一个地址到unsigned int的一个映射。Solidity语言中的哈希表的一个比较奇怪的地方是它不支持遍历,如果你想遍历哈希表中的所有元素,那么你自己需要想个办法记录一下哈希表中有哪些元素。我们这里用的bidders数组来记录的。
- Solidity语言中的数组可以是固定长度的,也可以是动态改变长度的。那么我们这里是一个动态改变长度的数据,如果你想往这个数组里面增加一个元素那就用push操作,bidders.push(bidder),就是在数组末尾新增一个出价人。如果你想知道数组有多少个元素,可以用它的bidders.length()。如果数组是一个固定长度的话,那么需要写明数组的长度,如address [1024] bidders。
- 接着看下面的代码,接下来是它的构造函数,Solidity语言中定义构造函数的方法有两种。一种方法就像c++中断构造函数一样,定义一个与contract同名的函数,这个函数可以有参数但是不能有返回值。新版本的Solidity语言实际上更推荐我们我们图中代码的方法,用constructor来定义一个构造函数,那么这个构造函数只有在合约创建的时候会被调用一次。
- 再下面是三个成员函数,这三个函数都是public,说明其他函数可以调用这些函数。我们注意一下这个bid()函数,这里有一个标志叫做payable,等会后面细说。还要注意一点,构造函数只能有一个。
9.2 智能合约的调用
(1)下面我们说一下,如何调用智能合约。
- 调用智能合约其实就是和转账是类似的。比如A发起一个交易转账给B,如果B是一个普通账户,那就和比特币中的转账交易是一样的。如果B是一个合约账户,那么这个转账实际上是发起一次对B合约的调用。
- 那么具体调用的是合约中的哪个函数呢?
这是在数据域中说明的,就是这个data域中说明。下图实例中,sender address 是发起调用的账户的地址,to contract address是被调用的合约的地址,调用的函数就是transaction data(图中红框标明的)。如果这个函数有参数,那么参数值也是在data域中说明的。
下图中中间这一行是调用的参数,value是说,我发起调用合约时,转过去多少钱。这里是0,因为我这个调用的目的仅仅是为了调用它的函数,并非是真的要转账。
GAS USED是我交易花了多少汽油费,GAS PRICE是单位汽油的价格,GAS LIMIT是这个交易我最多愿意支付多少汽油费,关于汽油费,后面细说。
(2)除了外部账户可以调用一个合约函数之外,一个合约也可以调用另外一个合约函数,那么调用的方法有如下:
- 第一种方法,如下图,
- 我们叫做直接调用,就是由A和B两个合约,A这个合约只是写一条log,event是定义一个事件LogCallFoo(),emit LogCallFoo()就是用emit操作来调用这个事件。emit语句的作用就是写一个log,对于程序的运行逻辑是没有影响的。
- 那么再看看B这个合约,这个合约的函数的参数是一个地址,就是A这个合约的地址,然后这个语句把这个地址转换成A合约的一个实例,然后调用其中的foo()函数。
- 注意:以太坊中规定,一个交易只有外部账户才能发起,合约账户不能够自己主动发起一个交易,所以下图例子中是需要有一个外部账户调用了合约B当中的函数callAFooDirectly(),然后这个函数再调用A当中的foo()函数。
- 第二种调用方法如下图,用地址类型,用address的call函数。
- 这时候第一个参数是要调用那个函数的签名,然后后面跟的是调用的参数。
- 这种调用方法和直接调用方法相比一个区别是对错误处理的不同。直接调用方法,如果你调用的合约在执行过程中出现了错误,那么会导致发起调用的合约也跟着一起回滚。
- 比如直接调用的图例中,A在执行过程中出现了异常,它抛出一个异常会导致B合约也跟着一起出错。而address的call这种方法,如果在调用中被调用的合约抛出异常,那么这个call()函数会返回false,表明这个调用是失败的,但是发起调用的这个函数并不会抛出异常,而是可以继续执行。
- 第三种调用方法是delegatecall(),和上面说的call方法基本是一样的。一个主要的区别就是,delegatecall()不需要切换到被调用的合约的环境中去执行,而是在当前合约这个环境中执行即可。也就是说直接用当前合约账户、余额和存储之类的。
(3)下面我们再看一下我们之前讲过的代码结构。
- 如下图,这个bid()函数有一个payable,而另外两个函数都没有。
- 说明一下,以太坊中规定,如果你这个合约账户要能接受外部转账的话,那就必须标注成payable。这个例子中bid()函数是用来竞拍出价的(这是一个网上拍卖的合约)。
- 比如你要参与拍卖,你出100个以太币,那么你就调用合约中的bid()函数。这个拍卖的规则就是,你调用bid()函数的时候要把你拍卖出价的100个以太币也发送出去存储到合约中,锁定到那里一直到拍卖结束,从而避免有人凭空出价。所以这个bid()函数要有能够接受外部转账的能力,所以才标注一个payable。
- 第二个withdraw()函数,它就没有payable,这个函数的作用就是拍卖结束了,出价最高的那个人赢得了拍卖,而其他人没有拍卖到自己想要的东西,那么可以调用这个withdraw()函数把自己当初拍卖的出价(就是调用bid()锁定在合约的以太币)再取出来。因为它不需要把钱转给智能合约,而仅仅通过withdraw()函数把当初锁定在智能合约里面的那部分钱取回来,所以没必要标注payable。
(4)我们再看看前面的例子,如下图,一个转账交易的例子,这里的value值就是0。
- 这个交易就属于并没有真的把钱转过去,所以这里面的to contract address这个函数就不用定义成payable。
- 以太坊中,凡是要接收外部转账的函数,都必须标志为payable。否则的话,如果这个函数没标注,你给这个函数转过去钱,就会引发错误处理,会抛异常。如果函数不需要接收外部转账,这个函数就不用写成payable。
(5)我们再来介绍一个特殊的函数叫做fallback()函数,如下图。
- 观察一下这个函数,它既没有参数,也没有返回值,也没有函数名,是个匿名函数。
- 我们叫做它fallback()函数,但是fallback关键字也没有出现在函数名里面。
- 我们前面介绍到,当你调用一个合约时,比如A调用B这个合约,然后要在转账交易里的data域说明你调用的是B合约中的哪一个函数。如果A给B转了一笔钱,但是没有说明调用的是哪个函数,又或者说它的data域是空的。这时该怎么办?那么这个缺省的时候就该调用这个fallback函数。
- 它叫做fallback函数这个名字是因为没有别的函数可以调用了那么就调用它。还可能你要调用的函数不存在,就是说你在你的data域中说明你要调用这个函数,但是实际合约中它没有这个函数,这时也得调用这个fallback()函数。
- 这个函数没有参数也没有返回值,是因为它没办法提供参数。对于fallback()函数来说,也可能需要标注payable这个关键词。如果fallback()函数需要有接收转账的能力的话,那么也需要注明payable,一般情况都是写成payable。如果你这个合约没有任何函数标志为payable(包括fallback函数),那么这个合约没有任何接收外部转账的能力。如果你非要往这个合约转钱,那就会引发异常。
(6)注意:
- fallback函数和payable都是在合约定义的时候写的。
- 如果你给一个合约账户转账,却没有调用它的任何一个函数,那就会自动调用这个fallback函数。
- 这个fallback函数不是必须定义的,这个合约可以没有fallback函数,如果没有这个函数,出现前面说的几种情况就会抛异常。如我给一个合约转账,没有说明调用的函数,合约中也没有fallback函数,那么这个转账就是错误的,就会引发错误处理。
- 只有合约账户有代码这东西,外部账户没这些东西。
- 另外转账金额可以是0,但是汽油费是要给的。这是两码事,转账金额是给收款人的,汽油费是给发布这个区块的矿工的。汽油费要是不给的话,矿工不会把你的交易打包发布到区块链上。
9.3 智能合约的创建和运行
(1)上面我们讲了智能合约的两种调用方法,那么智能合约是怎么创建的呢?如下图;
-
由某一个外部账户发起一个转账交易转给0x0这个地址,然后把要发布的合约的代码放到data域里面。这个转账金额是0,因为你实际上不是真的想转账,你只是想发布一个智能合约,你发布智能合约的代码放在数据域就行了。
-
你的合约代码写完后都要编译成bytecode,然后运行在EVM上。大家应该都听过JVM(java virtual machine),它的存在是为了增强可移植性。那么EVM也是类似的设计思想。通过增加一层虚拟机,对智能合约的运行提供一个一致性的平台,所以EVM有的时候叫做worldwide computer(全世界的一个计算机)。
-
EVM的寻址空间是非常大的,是256位。我们前面提到的uint是256位的。我们平时用的计算机大多都是64位的。
(2)我们这里详细介绍一下汽油费,如下图。
-
我们比较一下比特币和以太坊这两种区块链的编程模型。他们的设计理念是有很大差别的。比特币的设计理念是简单,脚本语言的功能很有限(比如说比支持循环)。而以太坊它就提供了一个图灵完备的编程模型(Turing-complete Programming Model)。
-
很多功能在比特币平台上实现起来很困难,甚至说根本实现不了。而到了以太坊平台上,实现起来就很容易,但是这也带来了一个问题;比如出现了死循环怎么办。
-
比如当一个全节点收到一个对智能合约的调用,怎么知道调用执行起来会不会导致死循环。答案是没办法知道。这实际上是个停机问题(halting problem),停机问题是不可解的。(这里注意一下,这个问题不是NPC问题,NPC问题是可解的只不过它还没有多项式时间的解法,很多NPC问题有指数时间解法。但是我们说的停机问题是不可解的,从理论上可以证明不存在这样一个算法,能够对任意给定的输入程序,判断出这个程序是否会停机,这是不可解的)
-
那么以太坊是怎么解决的呢?
办法就是将这个问题推给发起交易的账户。以太坊引入了汽油费机制。你发起一个对智能合约的调用,你就要支付相应的汽油费。我们可以看一下图中的交易的数据结构:
- AccountNonce—交易的序号,用于防止前面我们说的replay attack。
- Price和GasLimit就是和汽油费相关的,GasLimit就是我这个交易愿意支付的最大汽油量,Price是单位汽油的价格。所以这两个乘在一起就是交易可能消耗的最大汽油费。
- Recipient是收款人地址,就是转账交易转给谁的。
- Amount是转账金额,就是把Amount这么多钱转给Recipient。所以可以看出交易中的汽油费和转账金额是分开的。
- Payload就是我们前面说的data域,用于存放调用的是合约中的哪一个函数以及这些函数的参数取值。
(3)当一个全节点收到一个对智能合约的调用的时候,先按照调用中给出的GasLimit算出可能花掉的最大的汽油费,然后一次性把汽油费从发起账户上扣掉,然后再根据实际执行情况算出实际花了多少汽油费,多退少补(这个说法不大准确,如果你汽油费不够的话会引起回滚)。
(4)不同的指令消耗的汽油费是不一样的,一些简单的指令,如加减法,消耗的汽油费是很少的。复杂的指令消耗的汽油费就比较多;如去哈希,这个运算一条指令就可以完成,但是汽油费就比较贵。除了计算量之外,需要存储状态的指令消耗的汽油费也是比较大的。如果仅仅是为了读取公共数据,那么那些指令可以是免费的。
(5)下面我们介绍一下以太坊中的错误处理,如下图。
-
以太坊中的交易执行起来具有原子性,要么全部执行要么完全不执行,不会只执行一部分。交易既包含普通的转账交易也包含对智能合约的调用。所以如果在执行智能合约过程中出现任何的错误,会导致整个交易的执行回滚,退回到开始执行之前的状态,就好像交易完全没有执行过。那么什么情况会出现错误呢?
-
一种情况是我们之前提到的汽油费不足。如果交易执行完后,没有达到当初的GasLimit,那么多余的汽油费会被退回到账户里。就刚开始的时候是按照最大的GasLimit把汽油费扣掉了,如果最后运行完了还有剩下来的,那么实际上是用了多少汽油收多少钱,剩下的可以退回去。相反的,如果执行到一半,GasLimit已经用完了,那么这时候合约的执行要退回到开始执行之前的状态。这就是一种错误处理,而且这时候已经消耗掉的汽油费是不退的。
-
那么思考一下,为什么要这么做?
这是因为如果不这么做,就会有恶意的节点可能会发动Denial of Service Attack。它就可能发布一个计算量很大的智能合约,然后不停的调用合约,每次调用的时候给的汽油费给的汽油费都不够,反正最后还是会退回来,对于他本人来说没什么损失。但是这对矿工来说白白浪费了很多资源。 -
除了汽油费不足之外,还有一些情况也会引起错误处理。
- 比如说assert语句和require语句,这两个语句都是用来判断某种条件的,如果条件不满足的话那么就会导致抛出异常。
- assert语句一般来说是用于判断某种内部条件,这就有点像c语言中的assert。
- 这个require语句一般用来判断某种外部条件,比如说判断函数的输入是否符合要求。
(6)那么可以看看下图中的简单例子。
- 这个bid()竞拍函数,判断一下当前时间now是否小于等于拍卖结束时间auctionEnd。
- 如果符合条件的话继续执行,如果不符合条件的话,拍卖都已经结束了,你还在出价,那么这时候就会抛出异常。
- 图中第三个语句是revert(),这是无条件的抛出异常,如果你执行到revert这个语句,那么它自动地就会导致回滚。
- 早期的版本里面他用的是throw这个语句,新版本的solidity建议使用revert语句。最后注意一下,solidity里面没有try-catch这种结构。像java语言它有这种结构,用户可以自己定义出现异常后怎么办。
(7)我们前面提到智能合约执行过程中出现错误会导致回滚,那么如果是嵌套调用,一个智能合约调用另一个智能合约,那么被调用的智能合约出现错误是不是会导致发起调用的智能合约也跟着一起回滚,所谓的连锁式回滚。
- 回答是不一定,这取决于调用智能合约的方式。我们前面讲过两种调用方式。
- 如果是直接调用的话,那么它会触发连锁式的回滚,整个交易都会回滚。
- 如果是采用的call方式,它就不会引起连锁式回滚,只会使当前的调用失败返回false的返回值。
- 还得注意一点,有些情况从表面上看你并没有调用任何一个函数。就比如你往一个账户转账,如果这个账户是一个合约账户,你转账操作本身就有可能触发对函数的调用。这是因为fallback函数,这就是一种嵌套调用,一个合约往另一个合约里面转账就可能调用这个合约里面的fallback函数。
(8)如下图,这是我们之前看的block header的数据结构
- block header里面的GasUsed是这个区块里面的所有交易消耗的汽油费加在一起。
- GasLimit就是这个区块的所有交易能够消耗汽油的一个上限。这是因为发布区块需要消耗一定的资源,我们需要对区块消耗的资源有一个限制,比特币当中对于发布的区块有一个大小的限制,最多不超过1M。如果你发布的区块没有任何限制,那么有的矿工就可能把很多的交易全部打包到一个区块里面然后发布出去,那么这个超大的区块在区块链中会消耗很多的资源。
- 比特币的交易较为简单,可以用字节数来衡量交易消耗的资源多少。但是以太坊中这么规定是不行的,因为以太坊中智能合约的逻辑很复杂,有的交易从字节数上看很小但是它消耗的资源可能很大,比如它可能调用别的合约。所以我们要根据交易的具体操作来收费,也就是汽油费。
- 注意这并不是说把区块里面每个交易的GasLimit加在一起,因为每个交易的GasLimit是发布这个交易的账户自己确定的,定多少自己说了算。
- 除此之外再补充一点,比特币有1M的上限是固定的,是写在协议里面的,有些人认为这1M太小了,而且有的分叉币的产生就是想要提高这个上限。
- 但是以太坊的GasLimit也有一个上限,但是每个矿工在发布区块的时候可以对GasLimit进行微调,他可以在上一个区块的GasLimit基础上上调或者下调1/1024。例如,如果出现像比特币那种情况,大家都觉得GasLimit设得太小,协议写的不好,等到你发布区块的时候,你可以增加1/1024。这个1/1024看起来很小,但是以太坊出块速度很快,十几秒一个新的区块,很快就可以倍增;当然了如果有的矿工认为太大了也可以下调。所以说这种机制实际上求出的GasLimit是所有矿工认为比较合理的GasLimit的一个平均值。
9.4 关于智能合约的几个思考
(1)下面来思考一个问题,假设某个全节点要打包一些交易到一个区块里面,这些交易有一些是对智能合约的调用。那么这个全节点是应该先把这些智能合约都执行完后再去挖矿,还是说先挖矿获得了记账权再去执行这些智能合约?
-
观点1:先挖矿后执行智能合约。因为如果先执行智能合约,后挖矿,可能导致同一智能合约被不同节点执行多次,因此可能会导致一个转账操作被执行多次,即转账了好多次。
注意:实际上,这个观点很明显是没有理解区块链系统。一个在区块链上的区块中的智能合约,其必然在系统中所有节点中都得到了执行,因为这样才能保证系统中所有节点从一个状态转入另一个状态,从而保证系统的一致性。如果存在一个全节点没有执行该智能合约,那么该全节点的状态就和其他节点不一致,则该系统就没有保持状态一致。 -
观点2:先挖矿后执行智能合约。因为执行智能合约要收取汽油费,如果几个全节点都执行这个交易,会收取多份交易费。实际上这也是错误的。
-
观点3:先执行智能合约后挖矿。
实际上,这才是正确的。在介绍时候,我们常说执行智能合约时,要先从发起调用的账户扣除可能花费的最大汽油费,待执行完成后,有剩余再退还。这样介绍会令人感觉有些迷糊,那么每个节点都会执行智能合约,是不是每个节点都会扣除一份汽油费呢?
答:当然不是,这里就需要了解汽油费的扣除机制。
(2)汽油费是怎么扣除的?
- 首先,之前在以太坊数据结构中介绍了以太坊中“三棵树”——状态树、交易树、收据树。这三棵树都位于全节点中,是全节点在本地维护的数据结构,记录了每个账户的状态等数据。
- 所以该节点收到调用时,是在本地把该账户的余额减掉(一次性减GasLimit,如果余额不足就不执行)即可。所以多个全节点每人扣一次,仅仅是每个全节点各自在本地扣一次。
- 也就是说,智能合约在执行过程中,修改的都是本地的数据结构,只有在该智能合约执行完了被发布到区块链上之后,本地的修改才会变成外部可见,才会变成区块链的共识。
- 有很多的全节点,每个全节点在本地执行的智能合约可能不同。如果某个全节点发布一个区块,我收到这个区块后,我本地执行的就扔掉,然后再按照这个区块交易执行一遍,同时更新本地的三棵树。
(3)如果先挖矿后执行智能合约会如何?
-
我们回想一下以太坊的挖矿过程,其实就是尝试各种Nonce值,然后找到一个符合要求的。那么计算哈希值要用到什么,显然要用到block header的内容。block header中的Root、TxHash和ReceiptHash是前面说的三棵树的根哈希值。
-
所以你得先执行完区块中的所有交易包括智能合约的交易,这样才能更新这三棵树,才能知道这三个根哈希值,这样block header的内容才能确定,你才能够不断尝试Nonce值。所以说我们没办法先挖矿,只能先执行再挖矿。
-
但是这样也会带来一个问题,假设我是一个矿工,我费了半天劲执行这些智能合约,消耗了我很多资源,最后我挖矿还没挖到,那该怎么办?我能得到什么补偿吗?
显然是他得不到汽油费也得不到其他奖励,而且他还得把别人发布到区块里的交易在本地执行一遍,以验证发布区块的正确性,每个全节点都要独立验证。就是别人发布一个区块,你把区块里的所有交易在本地执行一遍,更新三棵树的内容,算出根哈希值再和之前他发布的根哈希值比较一下,看看是否一致,所有的这些都是义务劳动,没得补偿。 -
那么问题又来了,如果某个矿工想不通,不给钱,我就不验证了,我就假设你发布的区块是对的,我接着后面挖,就不验证。
答案:如果他跳过这个验证步骤,他以后就没办法挖矿。这是因为你验证的时候要把区块的交易都执行一遍,然后更新三棵树。如果你不验证的话,你本地的三棵树的内容没办法更新,以后你再发布区块,你算出的根哈希值在别人看来都是错的。 -
那么如果无法调过验证,我能不能直接把别人的验证结果抄过来。这个有点类似于矿池的做法,矿池其实就是很多矿工合在一起,矿工本身不验证,矿池有一个pool manager这个全节点。全节点负责统一验证,然后矿工就相信它验证完后的正确性。全节点分配给旷工的就是一个puzzle的内容,这个puzzle实际上是全节点通过跟着区块链更新得到的。
(4)发布到区块链上交易都是成功执行的吗?
答:为了防止恶意节点故意发布大量非法交易影响系统运行,对于其发布的交易即使无法成功执行也需要收取汽油费。但如果交易不被发布到区块链上,是无法收取汽油费的。发布出来让大家知道你为什么扣汽油费,扣得数目是否正确。所以发布到区块链上的交易不一定都是成功执行的。
(5)下面我们看看Receipt的数据结构,如下图,每个交易执行完成后会形成一个收据,下图便为收据的数据结构。其中status域就说明了该交易执行的状况。
(6)智能合约支持多线程吗?
-
solidity不支持多线程,根本就没有支持多线程的语句。
-
因为以太坊本质为一个交易驱动的状态机,就是给定一个智能合约,面对同一组输入,必须转移到一个确定的状态(或者说它产生的输出是确定的)。
-
因为所有的全节点都得执行同一组操作达到同一个状态,要进行验证。如果状态不确定,就会导致三棵树的根哈希值对不上。
-
但对于多线程来说,多个核对内存访问顺序不同的话,执行结果有可能是不确定的。此外,其他可能导致执行结果不确定的操作也不支持,例如:产生随机数。因此,以太坊中的随机数是伪随机数。
-
正是因为其不支持多线程,所以无法通过系统调用获得系统信息,因为每个全节点环境并非完全一样。因此只能通过固定的结构获取,下面章节就会介绍。
9.5 智能合约可得的信息
(1)智能合约特性:
- 智能合约的执行必须是确定性的,这也就导致了智能合约不能像通用的编程语言那样通过系统调用来获得一些环境信息。
- 因为每个全节点的执行环境不是完全一样的,所以它只能通过一些固定的变量的值来获得一些状态信息。
(2)下图就是智能合约能够得到的区块链的一些信息。智能合约没办法获得很精准的时间,只能获得跟当前区块信息相关的一些事件。
- msg.gas就是当前还剩下多少汽油费,这决定了你还能做哪些操作。
- msg.data就是所谓的数据域,这里面写了调用哪个函数以及这个函数的参数取值。
- 要注意:msg.sender(发起调用的人)和tx.origin(交易的发起者)是不一样的。如下图,我们有一个外部账户A,它调用了一个合约C1,C1中有一个函数f1;然后f1又调用了另外一个合约C2,C2中有个函数f2。那么对于f2这个函数来说,msg.sender是C1,但是tx.origin是A这个账户。
(3)下面我们在介绍一下智能合约的地址类型,如下图,
- 第一个是成员变量账户余额balance,剩下的都是成员函数。
- 第一个,以wei为单位的地址类型的余额中,uint256并不是指其包含一个类型为uint256的参数,而是指该变量本身为uint256类型的变量。
- 下面的函数语义与我们直观上的理解不是很一样,也与address.balance不同。
例如:address.balance指的是address这个账户的余额address.transfer(12345),并非address向外转账12345Wei,因为这样没有收款人的address。所以,该函数指的是当前合约向address地址中转入了12345Wei。 - 后面的函数都是类似语义,这里是需要注意的,因为其与我们认知存在差异。最下面三个函数为三种调用方式,前面具体介绍过。
(4)在以太坊中,转账有以下三种方法。如下图,
- transfer和send这两个专门是为了转账的函数。
- 区别在于,transfer会导致连锁性回滚(类似直接调用方法),失败时抛出异常;而send转账失败会返回false,不会导致连锁性回滚。
- call的方式本意是用于发动函数调用,但是也可以进行转账,也不会引起连锁式回滚,失败就返回false。
- 前两者在调用时,只发生大概2300wei的汽油费,这点汽油费很少,收到转账的合约也就只能写一个log,而call的方式则是将当前调用还剩下的所有汽油费全部发送过去(合约调用合约时常用call,没用完的汽油费会退回)。例如A合约调用B合约,而A不知道B要调用哪些合约,为了防止汽油费不足导致交易失败,A将自己所有汽油费发给B来减少失败可能性。
总结
五一假期结束前终于更完了以太坊相关的基础知识,如发现错误欢迎联系博主,本人会马上修改。另外下一次博客会介绍几个以太坊智能合约的相关项目。假期结束了,各位加油学习。另外博客更新不易,感觉本人写的不错的话,麻烦给个三连支持一下,谢谢。
以上是关于尚硅谷以太坊区块链学习的主要内容,如果未能解决你的问题,请参考以下文章