接上篇 文章,这里继续学习Solidity高级理论。
接下来,咱们将添加一些辅助方法。咱们为您建立了一个名为 zombiehelper.sol
的新文件,而且将 zombiefeeding.sol
导入其中,这让咱们的代码更整洁。前端
咱们打算让僵尸在达到必定水平后,得到特殊能力。可是达到这个小目标,咱们还须要学一学什么是“函数修饰符”。web
以前咱们已经读过一个简单的函数修饰符了:onlyOwner
。函数修饰符也能够带参数。例如:编程
// 存储用户年龄的映射 mapping (uint => uint) public age; // 限定用户年龄的修饰符 modifier olderThan(uint _age, uint _userId) { require(age[_userId] >= _age); _; } // 必须年满16周岁才容许开车 (至少在美国是这样的). // 咱们能够用以下参数调用`olderThan` 修饰符: function driveCar(uint _userId) public olderThan(16, _userId) { // 其他的程序逻辑 }
看到了吧, olderThan
修饰符能够像函数同样接收参数,是“宿主”函数 driveCar
把参数传递给它的修饰符的。segmentfault
来,咱们本身生产一个修饰符,经过传入的level参数来限制僵尸使用某些特殊功能。数组
_;
,表示修饰符调用结束后返回,并执行调用函数余下的部分。pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { // 在这里开始 modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } }
如今让咱们设计一些使用 aboveLevel
修饰符的函数。安全
做为游戏,您得有一些措施激励玩家们去升级他们的僵尸:服务器
是实现这些功能的时候了。如下是上一课的示例代码,供参考:网络
// 存储用户年龄的映射 mapping (uint => uint) public age; // 限定用户年龄的修饰符 modifier olderThan(uint _age, uint _userId) { require (age[_userId] >= _age); _; } // 必须年满16周岁才容许开车 (至少在美国是这样的). // 咱们能够用以下参数调用`olderThan` 修饰符: function driveCar(uint _userId) public olderThan(16, _userId) { // 其他的程序逻辑 }
changeName
的函数。它接收2个参数:_zombieId
(uint类型)以及 _newName
(string类型),可见性为 external
。它带有一个 aboveLevel
修饰符,调用的时候经过 _level 参数传入2, 固然,别忘了同时传 _zombieId
参数。zombieToOwner [_zombieId]
。zombies[_zombieId] .name
设置为 _newName
。changeDna
的函数。它的定义和内容几乎和 changeName 相同,不过它第二个参数是 _newDna(uint类型),在修饰符 aboveLevel 的 _level 参数中传递 20 。如今,他能够把僵尸的 dna 设置为 _newDna 了。zombiehelper.sol
oracle
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } // 在这里开始 function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } }
如今须要添加的一个功能是:咱们的 DApp 须要一个方法来查看某玩家的整个僵尸军团 - 咱们称之为 getZombiesByOwner
。app
实现这个功能只需从区块链中读取数据,因此它能够是一个 view
函数。这让咱们不得不回顾一下“gas优化”这个重要话题。
当玩家从外部调用一个view
函数,是不须要支付一分 gas
的。
这是由于 view
函数不会真正改变区块链上的任何数据 - 它们只是读取。所以用 view
标记一个函数,意味着告诉 web3.js
,运行这个函数只须要查询你的本地以太坊节点,而不须要在区块链上建立一个事务(事务须要运行在每一个节点上,所以花费 gas)。
稍后咱们将介绍如何在本身的节点上设置 web3.js。但如今,你关键是要记住,在所能只读的函数上标记上表示“只读”的external view
声明,就能为你的玩家减小在 DApp 中 gas 用量。
注意:若是一个
view
函数在另外一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是由于若是主调函数在以太坊建立了一个事务,它仍然须要逐个节点去验证。因此标记为 view 的函数只有在外部调用时才是免费的。
咱们来写一个”返回某玩家的整个僵尸军团“的函数。当咱们从 web3.js
中调用它,便可显示某一玩家的我的资料页。
这个函数的逻辑有点复杂,咱们须要好几个章节来描述它的实现。
getZombiesByOwner
的新函数。它有一个名为 _owner
的 address
类型的参数。external view
函数,这样当玩家从 web3.js 中调用它时,不须要花费任何 gas。uint []
(uint数组
)。先这么声明着,咱们将在下一章中填充函数体。zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } // 在这里建立你的函数 function getZombiesByOwner (address _owner) external view returns (uint []) { } }
Solidity 使用 storage
(存储)是至关昂贵的,”写入“操做尤为贵。
这是由于,不管是写入仍是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!须要在全球数千个节点的硬盘上存入这些数据,随着区块链的增加,拷贝份数更多,存储量也就越大。这是须要成本的!
为了下降成本,不到万不得已,避免将数据写入存储。这也会致使效率低下的编程逻辑 - 好比每次调用一个函数,都须要在 memory
(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。
在大多数编程语言中,遍历大数据集合都是昂贵的。可是在 Solidity 中,使用一个标记了external view
的函数,遍历比 storage
要便宜太多,由于 view
函数不会产生任何花销。 (gas但是真金白银啊!)。
咱们将在下一章讨论 for
循环,如今咱们来看一下看如何如何在内存中声明数组。
在数组后面加上 memory
关键字, 代表这个数组是仅仅在内存中建立,不须要写入外部存储,而且在函数调用结束时它就解散了。与在程序结束时把数据保存进 storage
的作法相比,内存运算能够大大节省gas开销 -- 把这数组放在view
里用,彻底不用花钱。
如下是申明一个内存数组的例子:
function getArray() external pure returns(uint[]) { // 初始化一个长度为3的内存数组 uint[] memory values = new uint[](3); // 赋值 values.push(1); values.push(2); values.push(3); // 返回数组 return values; }
这个小例子展现了一些语法规则,下一章中,咱们将经过一个实际用例,展现它和 for
循环结合的作法。
注意:内存数组 必须 用长度参数(在本例中为3)建立。目前不支持
array.push()
之类的方法调整数组大小,在将来的版本可能会支持长度修改。
咱们要要建立一个名为 getZombiesByOwner
的函数,它以uint []
数组的形式返回某一用户所拥有的全部僵尸。
result
的uint [] memory
(内存变量数组)uint
类型数组。数组的长度为该 _owner 所拥有的僵尸数量,这可经过调用 ownerZombieCount [_ owner]
来获取。result
。目前它只是个空数列,咱们到下一章去实现它。zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[]) { // 在这里开始 uint[] memory result = new uint[](ownerZombieCount[_ owner]); return result; } }
在以前的博文中,咱们提到过,函数中使用的数组是运行时在内存中经过 for
循环实时构建,而不是预先创建在存储中的。
为何要这样作呢?
为了实现 getZombiesByOwner
函数,一种“无脑式”的解决方案是在 ZombieFactory
中存入”主人“和”僵尸军团“的映射。
mapping (address => uint[]) public ownerToZombies
而后咱们每次建立新僵尸时,执行 ownerToZombies[owner].push(zombieId)
将其添加到主人的僵尸数组中。而 getZombiesByOwner
函数也很是简单:
function getZombiesByOwner(address _owner) external view returns (uint[]) { return ownerToZombies[_owner]; }
作法却是简单。但是若是咱们须要一个函数来把一头僵尸转移到另外一个主人名下(咱们必定会在后面的课程中实现的),又会发生什么?
这个“换主”函数要作到:
可是第三步实在是太贵了!由于每挪动一头僵尸,咱们都要执行一次写操做。若是一个主人有20头僵尸,而第一头被挪走了,那为了保持数组的顺序,咱们得作19个写操做。
因为写入存储是 Solidity 中最费 gas 的操做之一,使得换主函数的每次调用都很是昂贵。更糟糕的是,每次调用的时候花费的 gas 都不一样!具体还取决于用户在原主军团中的僵尸头数,以及移走的僵尸所在的位置。以致于用户都不知道应该支付多少 gas。
注意:固然,咱们也能够把数组中最后一个僵尸往前挪来填补空槽,并将数组长度减小一。但这样每作一笔交易,都会改变僵尸军团的秩序。
因为从外部调用一个 view 函数是免费的,咱们也能够在 getZombiesByOwner 函数中用一个for循环遍历整个僵尸数组,把属于某个主人的僵尸挑出来构建出僵尸数组。那么咱们的 transfer 函数将会便宜得多,由于咱们不须要挪动存储里的僵尸数组从新排序,整体上这个方法会更便宜,虽然有点反直觉。
for循环的语法在 Solidity 和 JavaScript 中相似。
来看一个建立偶数数组的例子:
function getEvens() pure external returns(uint[]) { uint[] memory evens = new uint[](5); // 在新数组中记录序列号 uint counter = 0; // 在循环从1迭代到10: for (uint i = 1; i <= 10; i++) { // 若是 `i` 是偶数... if (i % 2 == 0) { // 把它加入偶数数组 evens[counter] = i; //索引加一, 指向下一个空的‘even’ counter++; } } return evens; }
这个函数将返回一个形为 [2,4,6,8,10]
的数组。
咱们回到 getZombiesByOwner 函数, 经过一条 for 循环来遍历 DApp 中全部的僵尸, 将给定的‘用户id'与每头僵尸的‘主人’进行比较,并在函数返回以前将它们推送到咱们的result 数组中。
就是这样 - 这个函数能返回 _owner
所拥有的僵尸数组,不花一分钱 gas。
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[]) { uint[] memory result = new uint[](ownerZombieCount[_owner]); // 在这里开始 uint counter = 0; for(uint i = 0; i < zombies.length; i++) { if(zombieToOwner[i] == _owner) { result[counter] = i; counter ++; } } return result; } }
截至目前,咱们只接触到不多的 函数修饰符
。 要记住全部的东西很难,因此咱们来个概览:
private
意味着它只能被合约内部调用; internal
就像 private
可是也能被继承的合约调用; external
只能从合约外部调用;最后 public
能够在任何地方调用,无论是内部仍是外部。view
告诉咱们运行这个函数不会更改和保存任何数据; pure
告诉咱们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。这两种在被从合约外部调用的时候都不花费任何gas(可是它们在被内部其余函数调用的时候将会耗费gas)。modifiers
,例如在第三课学习的: onlyOwner
和 aboveLevel
。 对于这些修饰符咱们能够自定义其对函数的约束逻辑。这些修饰符能够同时做用于一个函数定义上:
function test() external view onlyOwner anotherModifier { /* ... */ }
在这一章,咱们来学习一个新的修饰符 payable
.
payable
方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种能够接收以太的特殊函数。
先放一下。当你在调用一个普通网站服务器上的API函数的时候,你没法用你的函数传送美圆——你也不能传送比特币。
可是在以太坊中, 由于钱 (以太), 数据 (事务负载), 以及合约代码自己都存在于以太坊。你能够在同时调用函数 并付钱给另一个合约。
这就容许出现不少有趣的逻辑, 好比向一个合约要求支付必定的钱来运行一个函数。
contract OnlineStore { function buySomething() external payable { // 检查以肯定0.001以太发送出去来运行函数: require(msg.value == 0.001 ether); // 若是为真,一些用来向函数调用者发送数字内容的逻辑 transferThing(msg.sender); } }
在这里,msg.value
是一种能够查看向合约发送了多少以太的方法,另外 ether
是一个內建单元。
这里发生的事是,一些人会从 web3.js
调用这个函数 (从DApp的前端), 像这样 :
// 假设 `OnlineStore` 在以太坊上指向你的合约: OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意这个 value
字段, JavaScript 调用来指定发送多少(0.001)以太。若是把事务想象成一个信封,你发送到函数的参数就是信的内容。 添加一个 value 很像在信封里面放钱 —— 信件内容和钱同时发送给了接收者。
注意: 若是一个函数没标记为
payable
, 而你尝试利用上面的方法发送以太,函数将拒绝你的事务。
咱们来在僵尸游戏里面建立一个payable
函数。
假定在咱们的游戏中,玩家能够经过支付ETH来升级他们的僵尸。ETH将存储在你拥有的合约中 —— 一个简单明了的例子,向你展现你能够经过本身的游戏赚钱。
uint
,命名为 levelUpFee
, 将值设定为 0.001 ether
。levelUp
的函数。 它将接收一个 uint
参数 _zombieId
。 函数应该修饰为 external
以及 payable
。require
确保 msg.value
等于 levelUpFee
。而后它应该增长僵尸的 level
: zombies[_zombieId].level++
。
zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { // 1. 在这里定义 levelUpFee uint levelUpFee = 0.001 ether; modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } // 2. 在这里插入 levelUp 函数 function levelUp(uint _zombieId) external payable { // 检查以肯定0.001以太发送出去来运行函数: require(msg.value == levelUpFee); zombies[_zombieId].level++; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[]) { uint[] memory result = new uint[](ownerZombieCount[_owner]); uint counter = 0; for (uint i = 0; i < zombies.length; i++) { if (zombieToOwner[i] == _owner) { result[counter] = i; counter++; } } return result; } }
在上一节,咱们学习了如何向合约发送以太,那么在发送以后会发生什么呢?
在你发送以太以后,它将被存储进以合约的以太坊帐户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。
你能够写一个函数来从合约中提现以太,相似这样:
contract GetPaid is Ownable { function withdraw() external onlyOwner { owner.transfer(this.balance); } }
注意咱们使用 Ownable
合约中的 owner
和 onlyOwner
,假定它已经被引入了。
你能够经过 transfer
函数向一个地址发送以太, 而后 this.balance
将返回当前合约存储了多少以太。 因此若是100个用户每人向咱们支付1以太, this.balance
将是100以太。
你能够经过 transfer
向任何以太坊地址付钱。 好比,你能够有一个函数在 msg.sender
超额付款的时候给他们退钱:
uint itemFee = 0.001 ether; msg.sender.transfer(msg.value - itemFee);
或者在一个有卖家和卖家的合约中, 你能够把卖家的地址存储起来, 当有人买了它的东西的时候,把买家支付的钱发送给它 seller.transfer(msg.value)
。
有不少例子来展现什么让以太坊编程如此之酷 —— 你能够拥有一个不被任何人控制的去中心化市场。
withdraw
函数,它应该几乎和上面的GetPaid
同样。a. 建立一个函数,名为 setLevelUpFee
, 其接收一个参数 uint _fee
,是 external
并使用修饰符 onlyOwner
。
b. 这个函数应该设置 levelUpFee
等于 _fee
。
zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { uint levelUpFee = 0.001 ether; modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } // 1. 在这里建立 withdraw 函数 function withdraw() external onlyOwner { owner.transfer(this.balance); } // 2. 在这里建立 setLevelUpFee 函数 function setLevelUpFee(uint _fee) external onlyOwner { levelUpFee = _fee; } function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[]) { uint[] memory result = new uint[](ownerZombieCount[_owner]); uint counter = 0; for (uint i = 0; i < zombies.length; i++) { if (zombieToOwner[i] == _owner) { result[counter] = i; counter++; } } return result; } }
咱们新建一个攻击功能合约,并将代码放进新的文件中,引入上一个合约。
再来新建一个合约吧。熟能生巧。
若是你不记得怎么作了, 查看一下 zombiehelper.sol
— 不过最好先试着作一下,检查一下你掌握的状况。
^0.4.19
.import
自 zombiehelper.sol
.contract
,命名为 ZombieBattle
, 继承自ZombieHelper
。函数体就先空着吧。zombiebattle.sol
pragma solidity ^0.4.19; import "./zombiehelper.sol"; contract ZombieBattle is ZombieHelper { }
优秀的游戏都须要一些随机元素,那么咱们在 Solidity 里如何生成随机数呢?
真正的答案是你不能,或者最起码,你没法安全地作到这一点。
咱们来看看为何
用 keccak256
来制造随机数
Solidity 中最好的随机数生成器是 keccak256
哈希函数.
咱们能够这样来生成一些随机数
// 生成一个0到100的随机数: uint randNonce = 0; uint random = uint(keccak256(now, msg.sender, randNonce)) % 100; randNonce++; uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
这个方法首先拿到 now
的时间戳、 msg.sender
、 以及一个自增数 nonce
(一个仅会被使用一次的数,这样咱们就不会对相同的输入值调用一次以上哈希函数了)。
而后利用 keccak
把输入的值转变为一个哈希值, 再将哈希值转换为 uint
, 而后利用 % 100
来取最后两位, 就生成了一个0到100之间随机数了。
这个方法很容易被不诚实的节点攻击
在以太坊上, 当你在和一个合约上调用函数的时候, 你会把它广播给一个节点或者在网络上的 transaction
节点们。 网络上的节点将收集不少事务, 试着成为第一个解决计算密集型数学问题的人,做为“工做证实”,而后将“工做证实”(Proof of Work, PoW)和事务一块儿做为一个 block
发布在网络上。
一旦一个节点解决了一个PoW, 其余节点就会中止尝试解决这个 PoW, 并验证其余节点的事务列表是有效的,而后接受这个节点转而尝试解决下一个节点。
这就让咱们的随机数函数变得可利用了
咱们假设咱们有一个硬币翻转合约——正面你赢双倍钱,反面你输掉全部的钱。假如它使用上面的方法来决定是正面仍是反面 (random >= 50
算正面, random < 50
算反面)。
若是我正运行一个节点,我能够 只对我本身的节点 发布一个事务,且不分享它。 我能够运行硬币翻转方法来偷窥个人输赢 — 若是我输了,我就不把这个事务包含进我要解决的下一个区块中去。我能够一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,而后获利。
因此咱们该如何在以太坊上安全地生成随机数呢 ?
由于区块链的所有内容对全部参与者来讲是透明的, 这就让这个问题变得很难,它的解决方法不在本课程讨论范围,你能够阅读 这个 StackOverflow 上的讨论 来得到一些主意。 一个方法是利用 oracle 来访问以太坊区块链以外的随机数函数。
固然, 由于网络上成千上万的以太坊节点都在竞争解决下一个区块,我能成功解决下一个区块的概率很是之低。 这将花费咱们巨大的计算资源来开发这个获利方法 — 可是若是奖励异常地高(好比我能够在硬币翻转函数中赢得 1个亿), 那就很值得去攻击了。
因此尽管这个方法在以太坊上不安全,在实际中,除非咱们的随机函数有一大笔钱在上面,你游戏的用户通常是没有足够的资源去攻击的。
由于在这个教程中,咱们只是在编写一个简单的游戏来作演示,也没有真正的钱在里面,因此咱们决定接受这个不足之处,使用这个简单的随机数生成函数。可是要谨记它是不安全的。
咱们来实现一个随机数生成函数,好来计算战斗的结果。虽然这个函数一点儿也不安全。
randNonce
的 uint
,将其值设置为 0。randMod
(random-modulus)。它将做为internal
函数,传入一个名为 _modulus
的 uint,并 returns
一个 uint
。randNonce
加一, (使用 randNonce++ 语句)。zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiehelper.sol"; contract ZombieBattle is ZombieHelper { // 在这里开始 uint randNonce = 0; function randMod(uint _modulus) internal returns (uint) { randNonce ++; return uint(keccak256(now, msg.sender, randNonce)) % _modulus; } }
咱们的合约已经有了一些随机性的来源,能够用进咱们的僵尸战斗中去计算结果。
咱们的僵尸战斗看起来将是这个流程:
这有一大堆的逻辑须要处理,咱们将把这些步骤分解到接下来的课程中去。
uint
类型的变量,命名为 attackVictoryProbability
, 将其值设定为 70。attack
的函数。它将传入两个参数: _zombieId
(uint 类型) 以及 _targetId
(也是 uint)。它将是一个 external
函数。zombiehelper.sol
pragma solidity ^0.4.19; import "./zombiehelper.sol"; contract ZombieBattle is ZombieHelper { uint randNonce = 0; // 在这里建立 attackVictoryProbability uint attackVictoryProbability = 70; function randMod(uint _modulus) internal returns(uint) { randNonce++; return uint(keccak256(now, msg.sender, randNonce)) % _modulus; } // 在这里建立新函数 function attack(uint _zombieId, uint _targetId) external { } }