Dapp开发:心理准备(上)
为想创建项目的朋友搭建创业平台,请感兴趣的朋友加乐乐微信:sensus113
NervosFans 微信公号:Nervosfans
谢谢!
一篇致Web开发者的区块链应用开发教程。
简介
客户端 - 服务器 - 数据库是多数Web应用的主流架构。Flow一般是用户从笔记本电脑、智能手机还有平板上的客户端发出请求,提交至后端服务器,再由服务器与数据库通信对数据进行检索。 客户端与服务器/数据库之间的关系是多对一的,由后者做集中控制。 如果说Web
1.0与静态网页有关,那么Web 2.0就是与交互和JavaScript有关的。
web stack:Clients=>Servers=>Databases
我们想要抵达的目的地,其实是Web 3.0这片服务器、数据库的去中心化程度与客户端相当的热土。 换言之,客户端既能充当服务器也能充当数据库,甚至是两者皆有之(也叫点对点)。 堆栈的各个层都是多对多的关系时,就不存在所谓的集中控制与单点故障,此时网络实现了最大程度地分布。
Web 1.0 静态(HTML/CSS)
Web 2.0 交互(JavaScript)
Web 3.0 去中心化(区块链)
客户端为何还要维护数据库?比特币就是个标准例子。人们共同维护一个有关余额与交易的公共分类账时,也就创造出了一种完全脱离政府、机构存在的全球货币。Satoshi Nakamoto大概是算准了时机,在2009年1月3日全球金融危机的顶峰,挖出了第一枚比特币。
blockchain => public ledger => global currency
不久之后,Vitalik Buterin意识到,若客户端能将“服务器”与数据库一起维护,就能实现底层区块链的可程控,再进一步就是令人惊奇的自主智能合约。对这些合约编程后,实现了现实世界资产的通证化(数字化)的同时,所有权记录不可变更。 如此,来自世界任何一个角落的任何人都能以价低且安全地方式进行任何交易,直接跳过有腐败温床之称的中介机构。 大到土地、能源,小至音乐、投票,可应用的场景之多,影响之深刻。
smart contract=> records of ownership => blockchain
利用了智能合约的应用程序则被称为去中心化应用,简称Dapps。 Dapps是与智能合约(而非服务器)连接的前端应用程序,用于在区块链(而非数据库)上保留或检索数据。 客户端通过外部帐户与dapps交互。作者想就构成区块链应用堆栈的各架构层分享一个简单的心智模型,希望能对那些有兴趣做以太坊dApp开发的Web开发者有所帮助。
webapp: Client =>Server=>Database
dapp: External Account => Smart Contract =>Blockchain
有关区块链的非技术性介绍,可查阅Bertie Spell这篇有见地而且还挺有趣的入门读物:
https://bertiespell.com/blockchain-solves-everything
区块链(数据库)层
区块链层类似数据库层,是保存并检索数据的地方。 此处,由区块记录发生的每笔交易。区块链的不同之处在于任何人都可以对其做添加。共识则通过一种叫做挖矿的过程实现,具体指运行以太坊客户端的计算机们竞争挖出下一个区块(也叫工作量证明)。 这些节点共同组成了一个网络,每个节点都保留整个区块链的副本。 这种新颖做法也解决了困扰分布式系统多年的拜占庭将军问题。
network: Ethereumclients => mine(blocks) => proof
向区块链添加新数据时,用户向网络中的各节点提交交易。 随后,由节点在挖矿前将待处理交易分组进各个区块。
node: transactions => blocks =>blockchain
确切的讲,每个区块都是交易的Merkle树 + 收据的Merkle树 + 状态的Merkle树。 这些Merkle树(准确的说是Merkle-Patricia tries)的好处是简化了验证过程(又名Merkel证明),验证支付时,节点下载区块头即可,不用把每个区块中的每笔交易来下载下来(就是轻客户端了)。
区块头:
1) transactions =>transactions trie=> transactionsRoot
2) infos and logs =>receipts trie => receiptsRoot
3) addresses and balances =>state trie=> stateRoot
transactionsRoot是区块中交易的trie,receiptsRoot 是交易信息与日志的trie,而stateRoot是帐户地址、余额(也叫分类帐)的trie。 本质上讲,交易属于状态迁移,智能合约就是状态迁移函数了。
transactions: genesis =>state 1 => state 2 => ... => state N
底层数据结构就是加密哈希二叉树的链表。
blockchain: genesis <= tries 1 <= tries 2 <= ... <= tries N
挖掘新区块时,需要计算出区块头中这些Merkle根的哈希。 此哈希引用上一个区块的哈希的同时还要计算出一个特殊的nounce,让值小于某个目标(值)。 这项算数工作就是该区块有效的“证明”。
Hash (nounce, block header, previous hash)<target
目标值由Ethash算法设定,算法会考虑解决上一个区块所花费的时间,使得平均出块时间统一在15秒。 由于运行节点的人数不固定且计算资源的质量也在不断提高,这个目标会不断被调整。 输出哈希为十六进制(基数16,通常以“0x”为前缀),总长度为64个字符。
https://etherscan.io/chart/blocktime出块时间过高(目标值过低)则网络速度减慢,交易处理时间变长。出块时间过低(目标值过高)则产生多个重复的解决方案(计算结果),导致区块链分叉。
所以说,15秒的出块时间还是比较理想的。
lowtarget value=>high block time => takestoo long
hightarget value=>lowblock time => too manyforks
出现分叉时,以太坊遵循GHOST协议,由协议选出已完成最多“工作量”的分叉,即最长链(区块编号最高)。
Fork A: genesis + block1 + block 2 + ... + block 12 // main
Fork B: genesis + block1 + block 2 + ... + block 11 // uncle
Fork C: genesis + block1 + block 2 + ... + block 10 // uncle
与比特币不同,以太坊中的分叉不是“孤叉”,所以不会被遗弃。这些 “叔伯叉”也有奖励,以此分散矿池,提高安全性。一般来说,需要等待至少6次网络确认,让最长链条大幅获胜。
block reward: 3 + fees + (1/32x uncles)
uncle reward: 7/8 x 3 // 2uncles per block (maximum)
挖矿越集中,矿工串通起来验证双重支出的51%攻击风险就越大。等待网络确认至少6个以上的区块则可以降低这种风险。 其实,这种攻击太烧钱而且无利可图,譬如至少要砸60亿美元才能实施攻击,考虑到不诚实行事并获得回报的机会成本,砸的会更多。
看看以太坊网络上交易区块实际长什么样:
https://etherscan.io/block/5912705区块高度表示了区块在链中的顺序。 图中区块的编号为5912705。 根据时间戳,该区块于2018年7月5日上午12点左右由Ethermine挖出。 区块包含89笔交易,相当于19290个字节的新数据。Ethermine第一个确定了让哈希值小于目标难度的正确Nounce。作为奖励,Ethermine收到3.46486964996 ETH(区块奖励)。
block reward = coinbase + fees +uncles = 3 + 0.46486964996 + 0
Gas代表执行代码所需的计算操作。 区块gas上限规定了区块中所有交易能“消耗”的最大操作数量。 类似比特币的块大小限制,设限的目的是为了将交易处理时间与传播至其他节点的时间保持在较低水平。与比特币不同的是,gas上限不是一成不变的,矿工(通过投票的方式)可以对上限进行小范围的调整。
block gas limit= 1.5 x 10^6 x π ± 1/1024
sum(transaction gas limits) <block gas limit
跟比特币一样,矿工成功解决加密难题时,就有新的ETH被创建,并以奖励的方式直接加到矿工账户余额中(也叫coinbase交易)。不过,比特币中的coinbase交易需要由矿工发行。
现在,我们来看看其中的两笔 transactions:
https://etherscan.io/tx/0xc28ab33c5943d0d593d34d9af1b29971310dac009b88b83d3b2fae5dfdfed327 https://etherscan.io/tx/0x0875ed575d905efdbb909020da5f83867aadfa572f4164ff009a94025f4a977e这两笔交易代表了以太坊中两种可能的交易类型。将ETH发送到另一个账户(第一笔交易),或者发送到合约账户(第二笔交易)。
1) externalaccount =>externalaccount
2)externalaccount => contractaccount
3)contract account => contract account // "internal" transactions
这两笔交易说明了TimeStamp、From、To以及要发送的ETH数量(Value)。还表明了可用gas数量(Gas Limit)、消耗gas数量(Gas Used By Txn)以及单位成本(Gas Price)。 这里的Nounce只是一个计数器,用于为发件人提交的全部交易排序。 要知道排序是十分有必要的,原因是交易必须按顺序处理,防止透支(本那么多钱可花的意思)。 交易提交后,交易TxHash被创建。使用TxHash可以ping命令网络检查自己交易的TxReceipt Status。交易用于合约账户时,可以附上Input Data供合约使用。
GasUsed By Txn < Gas Limit
GasUsed By Txn x Gas Price =Actual TxCost/Fee
用户提交交易就代表需要网络中的节点对其进行处理。而处理交易会消耗计算资源,这笔费用以gas的形式由用户支付。Gas费用不仅能够阻止垃圾交易,还能够阻止诸如无限循环之类的不良代码。Gas数量以及价格可以制定。这个费用会在一开始就被扣除,执行结束后gas有剩则退回。需要注意的是,指定的gas不足时,交易无法完成且gas不退。每单位gas花的钱越多,交易优先级越高。目前的话,gas价格(设成)50 gwei(0.5美元),处理时间是1分钟。与汇款转账相比,不管是速度还是费用这都很良心了。
gasprice=supply(miners) + demand(external accounts)
不过,有个令人不解的地方是,合约账户也有一个nounce,但是有其他意思。 这些情况下,每当合约帐户创建另一个合约帐户时,这个数字就会递增。
blocknounce =solutionto the cryptographic puzzle
external accountnounce = number oftransactions issued
contract accountnounce = number ofcontracts created
结束本节之前,我们来看下外部账户与合约账户的构成:
图片: https://etherscan.io/address/0x68b42e44079d1d0a4a037e8c6ecd62c48967e69f https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d这两种类型的帐户都能发送、接收ETH。 所以说,两者都有一个跟踪ETH的Balance字段。 但是,合约帐户也能存储数据,所以还有另外有个storage字段和code字段,字段中包含操作存储数据方法的机器指令。 换言之,合约账户是宿在区块链上的智能合约。
contractaccount = balance + code + storage
externalaccount = balance + empty + empty
外部帐户由(人类的)私钥控制,而合约帐户由代码控制。 两个外部账户之间的交易代表的仅是价值转移。 外部帐户到合约帐户的交互则能激活被调用帐户的代码。 注意,跟我们一般理解的正好相反,智能合约其实并不是自执行的,合约帐户的各种活动始终由外部帐户启动。
external account=> transaction =>contract account A => "internal" transaction => contractaccount B => ...
以太坊(and 比特币)中,ETH只有被发送的份儿,‘强取’是绝对不被允许的。 所以,From字段的安全性就极为重要了,重要到用了三个其他的字段来确定其有效性。这三个字段分别是v、r和s。 显然,成功诓骗From地址就意味着能够操纵转移帐户中全部ETH。 为了更好的理解v、r和s,我们先了解下非对称加密。
transact =sendethers // can never take ethers
加密技术被用来对消息进行加密、解密。对称加密指加密、解密用同一把密钥,而非对称加密指的是解密只能用私钥,而任何人都能用(我们的)公钥加密(只有我们能看到的)消息。
//Symmetric Cryptography(对称加密)
encrypt(unencrypted message,key 1) => encrypted message
decrypt(encrypted message,key 1) => unencrypted message
//Asymmetric Cryptography(非对称加密)
encrypt(unencrypted message,public key) => encrypted message
decrypt(encrypted message,private key) => unencrypted message
这种属性也使得非对称加密更为安全,因为公私(钥)有别。 非对称加密的另一个优点是私钥可以用来“签名”消息,而后他人可以使用公钥验证签发的消息与签名是否匹配。
sign (message,private key) => signature
verify(message, signature,public key) => true/false
以太坊中,私钥是个随机的64位十六进制字符。Base 10中就非常大了,而且无从猜测。使用椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm/ECDSA))可从私钥导出公钥,获得128位十六进制字符。 生成帐户地址时,使用Ethash(keccak-256)对公钥做哈希,删除前24个字符,最终生成40位十六进制字符。
random (hexadecimal characters) =>private key// 64 characters
ECDSA (private key) =>public key// 128 characters
keccak (public key) =>account address// 40 characters
以太坊中,r和s是用发送者私钥签名交易对象的ECDSA输出,v用于计算起始地址。
sign(transaction) =>rand s(signature)
v=> public key => accountaddress
这些共同参数构成了网络中节点验证交易确实源自某帐户地址的手段。
verify(transaction, rs_signature, v_address) => true/false
起始地址可以从v中导出,所以不用与交易一同提交。
最后,可从帐户地址与交易nounce生成合约地址:
keccak(account address, transaction nounce) =>contract address// 40 characters
忽略前12个字符,得到40位十六进制字符。
由于外部地址与合约地址都来自私钥,那么只要私钥安全,就不会发生盗窃事件。 相反,若丢了私钥,也就可以跟自己的ETH永远的说拜拜了。
智能合约(服务器)层
智能合约层类似于业务/控制器逻辑所在的服务器层。 创建智能合约时,将逻辑作为外部帐户的有效负载,提交交易至“空白”收件人即可。
externalaccount => "blank" transaction=>contractaccount
区块链中,智能合约作为一种带存储和代码字段的“内部”帐户存在。 这些字段分别保留了智能合约的数据与逻辑。 因此,智能合约可以被抽象为类构造,并且可以发明出一种图灵完备(Turing-complete)的编程语言。 Solidity就是这样一种语言,而且与JavaScript相似,所以颇受欢迎。
storage + code =properties+methods=>class=> Solidity
Solidity中,类构造具有关键字合约。 与JavaScript类似,可以在这些合约类中定义变量与函数(也叫属性与方法)。
pragmasolidity ^0.4.24;
contract{
variables;
constructor {}
functions {}
}
与类相似,任一合约都能承继其他合约:
pragmasolidity ^0.4.24;
import"/Foo.sol";
contractBar is Foo{
...
}
此处,Bar从Foo承继,意思是Foo的状态以及函数被迁移至Bar。
inheritance: base contract => derived contract
//inherits variables and functions, which can be overridden
与Bar的函数名称、输入和输出相同时, Foo的函数被覆盖。 名称相同但输入、输出不同,函数被重载。
functionfoo(uint a) {}
functionfoo(uint a) {} // overridden
functionfoo(uint a, uint b) {} // overloaded
超过一个合约被承继时,必须从最基层(most base)到最底层(most
derived)对承继排序。
contractA {}
contractB is A {}
contract C is A, B {}// will not compile!
contract C is B, A {}// will compile
倾向组合(而非承继)时,合约无需承继也可调用library函数:
pragmasolidity ^0.4.24;
libraryFoo {
function foo() {
...
}
}
contractBar {
function bar() {
Foo.foo();
}
}
尽管单个合约可以继承多个其他合约,但在区块链中,代码被合并为一个合约账户。另一方面,来自库的代码独立存在于区块链上。
inheritance: multiple smart contracts => one contractaccount
区块链上的库帐户既没有存储空间,也无法持有ETH。调用库函数时,会传入调用合约的存储及余额(也叫上下文)。库合约只有被部署后才能被链接,原因依赖库合约的其他合约需要其在区块链上的地址。
1.deploy(library) => libraryaddress
2.deploy(contract(library address)) => contract address
Solidity支持面向对象的设计。可以创建其他人可以继承的抽象合约,函数签名足矣。
pragmasolidity ^0.4.24;
contractFoo {
function foo() public returns (bytes32);
}
contractBar isFoo{
...
}
还可以创建接口合约。这里仅允许函数签名,因此不存在构造函数、变量以及承继。
pragmasolidity ^0.4.24;
interfaceFoo {
function
foo() public returns (bytes32);
}
Solidity文件名应与其合约类相同,扩展名为.sol。 编译文件时,用pragma语句指定要使用的Solidity版本。 编译过程的输出为bytecodes与application binary interface(ABI)。
pragma solidity ^0.4.24
semver=> major.minor.patch
^ => the version indicated up to but notincluding the next major version
~=> the version indicated up to but notincluding the next minor version
compile(contract.sol) => bytecodes + ABI
部署智能合约时,提交附加了字节码的“空白”交易。每次合约被部署,都会创建其“实例”并调用其constructor。 除了字节码,还可以附上自己想要传递给构造函数的参数。
contractclass=> bytecodes + constructor arguments => contractinstance
类似JavaScript Engine,以太坊虚拟机(EVM)驻留在每个节点中,负责执行字节码。 EVM运行时,维护一堆(stack)可以写入合约存储树的操作码。 堆栈最多可包含1024个元素,每个元素32个字节。这里不建议使用递归,原因是很有可能导致堆栈溢出。 本质上讲,EVM就是一个状态机,由操作码指定如何将状态迁移应用于下一个块状态。
EVMstack= opcode + opcode + ... + opcode => next block state
操作码执行起来有便宜有贵了:
操作 Gas 描述
ADD/SUB 3 算术运算
MUL/DIV 5 算术运算
ADDMOD/MULMOD 8 算术运算
AND/OR/XOR 3 按位逻辑运算
LT/GT/SLT/SGT/EQ 3 比较运算
POP 2 堆栈操作
PUSH/DUP/SWAP 3 堆栈操作
MLOAD/MSTORE 3 存储操作
JUMP 8 无条件跳转
JUMPI 10 有条件跳转
SLOAD 200 存储器操作
SSTORE 5,000/20,000 存储器操作
BALANCE 400 从账户调取余额
CREATE 32,000 使用CREATE创建新账户
CALL 5,000 使用CALL创建新账户
合约创建交易期间,EVM执行构造函数初始化新合约帐户。 初始执行生成的代码就是构成特定“实例”的代码。
EVM(bytecodes, constructor arguments) =>contractinstance(s)
这就意味着可以部署任意数量智能合约类的“实例”。
ABI是合约实例中通往操作码的开发者门户。 其功能是将字节码转换为对网络的JSON RPC调用。
developer => ABI=> RPC(bytecodes) => contract instance
想与区块链上的实例交互,可以使用ABI指示EVM要使用哪些参数调用哪些函数。 最受欢迎的JavaScript库是web3,大有‘构建未来Web3.0,有你有我’的意思。
web3(ABI) =>EVM(functions, arguments)
每个web3调用要么是交易(发送)要么是调用。 交易被发送到网络而且可能会改变状态。 调用是只读且快速的。 合约可以调用其他合约(也就是消息),但是记住一点,永远由一笔交易触发一些事件。也就是说,消息调用本身尽管永远不会改变状态,但是可以作为改变状态的交易的一部分。
transaction(send) => message call(s) => statechange
call=> no state change
有了web3,可以提交四种类型的交易:
1. 从一个外部账户向另个外部账户发送ETH(类似比特币交易)
2. 发送空白交易部署智能合约(成为合约账户)
3. 从一个外部账户向合约账户发送ETH
4. 发送交易执行合约账户中的方法(更新或检索合约状态,或调用其他合约)
交易与调用以操作码的方式由EVM执行,意味着会有gas费用。 更新区块链或合约状态的交易需要支付ETH。 交易会花些时间但总能返回交易哈希。 从区块链或合约中检索数据的调用则是“即时”且免费的。 可以从这些调用中返回任意值。
setterFunction(ethers) => transaction hash // takes time
getterFunction(free) => any data // happens"instantly"
由于节点竞争挖掘出下一个区块,因此网络上冗余地执行各种函数调用。 为了抵消这种对计算资源的浪费,最佳做法是尽可能多地执行链下计算。
off-chainwork => on-chain work
Solidity尽管在语法上类似JavaScript,也还是有几处主要区别。 首先,变量属于强类型,有两种类型。 状态变量属于原语,而引用变量指向状态变量的集合。可隐式转换时,两种类型可以强制转换成对方。
状态变量:
• bool:布尔值(true 或 false)// 默认false
逻辑运算符:
! //logical negation(非)
&& // logical conjunction(与)
|| //logical disjunction(或)
== //equality
!= // inequality
条件陈述遵循短路规则
• int/uint:正或负整数(28 到2256)//默认为 0
- 可使用或不使用数字后缀定义
- 后缀必须为8的倍数
uint = uint256
uint8, uint16, uint64, ... , uint256
int = int256
int8, int16, int64, ... , int256
• fixed/ufixed:正或负浮点数// 默认为 0
fixedMxN or ufixedMxN
// M = number of bits taken by the type;
between 0 and 80(类型所占比特数量,0-80之间)
// N = how many decimal points are available;
between 8 and 256; must be divisible by 8(小数点后几位,8-256之前,必须能被8整除)
ufixed = ufixed128x18
fixed =fixed128x18
• address:20字节一个以太坊地址(40 个十六进制字符) //默认为 0x
.balance
// get balance
.transfer()
// send ether from current contract account to
// errors will throw
.send()
// low-level counterpart to transfer()
// not recommended because errors are silent(return false)
还可以通过调用所有addresses中内置属性与方法的方式访问帐户变量以及函数。 想要获取帐户余额,可以调用balance。 要从calling合约账户向其他地址发送ETH,可以使用transfer()(这里不推荐用send())。
.balance=> balance in wei
.transfer()=> transaction hash
想要与其他合约帐户交互,可以使用call和delegatecall(这里不推荐使用callcode)执行其中的函数。
.call()
//call another contract
//return boolean
//additional modifiers: .gas() or .value()
.callcode()
//deprecated
.delegatecall()
//delegates a function call to another contract
call做“外部”执行,delegatecall做“内部”执行。 就是说,delegatecall执行另一个合约的函数。 类似调用库函数,delegatecall能够在运行时从其他合约动态加载代码。Calling合约的上下文(即地址、余额以及存储)被保留,仅获取被调用合约的代码。
External:
.call("foo", 1)
Internal:
.delegatecall(bytes4(keccak256("bar(uint)")),2)
.bar(2)
参考变量:
• 固定数组:单一类型长度不变元素数组(byte、type[N])
byte[N] // 0 < N < 33
- 可分配至存储或内存
- 存储数组可为任意数据类型
- 内存数组不能为映射,其他均可
- 声明数组为public时, getter 函数被创建,函数需要期望值的索引做参数
• 动态数组:单一类型长度变化元素数组(string, bytes,
type[])
bytes = byte[]
.length
// returns the length of the array(返回数组长度)
// dynamic arrays in storage can be resized by
assigning a length(存储中的动态数组的长度可重新指定)
.push()
// appends a value to the array
// new length is returned
// storage arrays and bytes (not strings)
uint[]memorya =newuint[]()
// variable length can be defined at runtime
by using the new keyword(可使用新关键字在运行时定义变量长度)
// once defined, it will be of fixed size(定义后大小不再改变)
• 映射:同类型键值对集合
mapping (<key type> => <value
type>)
// key type can be anything but mapping,
dynamic array, contract, enum, or struct(键类型不能为映射、动态数组、合约、枚举或结构)
// value type can be anything(值类型无限制)
// mapping is basically a hash table(映射就是个哈希表)
// every value is initialized to its default(所有值都被初始化至默认值)
// no length property(无长度属性)
// key data is not stored, only its keccak256
hash(仅存储键数据的keccak256哈希)
// declaring the mapping public creates a
getter function that requires the key of the desired value as a parameter(声明映射为public时,getter 函数被创建,函数需要期望值的键做参数)
• 结构:不同类型键值对集合
- 定义新类型的方法
- 不能包含同类型成员
- 由引用传递以位置变量形式存储的struct值
• 枚举:有限自定义类型集合
- 用户自定义类型
- 可显式转换为整数
- 至少一个成员
类似计算机硬盘、RAM,storage变量指向持久状态,而memory变量指向临时对象。
storage variable => persisted across executions //expensive
memoryvariable => persisted during execution //cheap
显然,状态变量总是在存储中,而引用类型的局部变量默认存在(存储中),使用memory关键字可以将其复制到内存中。 换言之,这些变量由引用传递。 另一方面,默认下,函数参数与返回参数在内存中,由值传递。 要通过引用传递(这些参数),可以使用storage关键字。 值类型的局部变量存储在EVM堆栈中,直到执行完成。
storage: state variables (always), local variablesof reference type (default)
memory: function parameters (default), returnparameters (default)
call stack: local variables of value type (always) //cheapest
与内存类似的一个区域叫做calldata,其中存储了外部函数的参数。
calldata: external function parameters
显然,引用变量引用的是storage中的变量,清楚起见,仍需指定storage关键字。 要将引用的变量复制到memory,可以使用memory关键字。
mapping(address => uint) balances;
uintstoragebalance = balances[msg.sender];
// 指向存储中发送人的余额
// 复制发送人余额进内存
错误可能隐藏在变量存储位置的假设中。 这种做法阻止了这一点
跟JavaScript一样,Solidity函数也是第一类构造,意思是函数能作为参数传递给其他函数,或者由(其他函数)返回。 因此,有两种类型的函数。 内部函数由同一上下文中的其他函数在内部调用(也叫为合约间消息调用)。 外部函数通过交易(来自外部帐户或其他合约)从外部调用。 默认下,函数都是内部函数,但可以使用外部关键字进行变更。
External: EVM => contract A => function 1
Internal: contract A (function 1 => function 2)
由于合约可以承继其他合约,还可以明确地将函数的可访问性指定为公共或私有。 公共函数可以通过交易从外部访问,或通过派生(derived)合约做内部访问,而私有函数只能由当前合约访问。可访问性未定义时,则认为函数为公共函数。
Public: contract A (function 1) => contract B (function 1)
Private: contract A (function 1) => contract A (function 2)
尽管public与external指定都允许从外部访问函数,还是推荐外部调用的方式。原因是公共函数中,EVM会把参数复制进内存,而外部函数仅读取负载中存储的参数。而复制这一步往往成本较高,特别是当参数比较大的时候。
public functions: write arguments to memory // expensive
external functions: read arguments in payload // cheap
还可以指定变量的类型与可访问性。 作为合约的“内部”,状态变量始终是内部的。 跟函数一样,状态变量可以是公共的也可以是私有的,具体取决于是否希望派生合约访问其。 方便起见,可以为公共变量创建自动getter函数。
public variable => variable()
注意,尽管私有指定能够阻止其他合约访问并修改其数据,但是区块链的本质是合约内的所有内容对所有外部观察者可见。 其实,区块链上的东西并没有真正被删除。对合约状态进行变更,也不意味着“覆盖”了任何内容。 虽说可以使用selfdestruct操作“移除”合约存储以及代码区中的数据,但节点可以选择无限期地保留它们。
selfdestruct(recipient) =>contract "deleted"
函数还带有默认修饰符。 清楚起见,不修改合约状态的函数应标记为pure或view。view函数需要从状态读取,而pure函数则不需要。 修改状态的函数应始终返回交易哈希,且仅返回哈希。 处理ETH的函数应指定为应付。为便于添加其他行为,鼓励代码重用,还可以使用自定义修饰符修饰函数:
modifier onlyOwner() {
require(msg.sender ==owner);
_;
}
function foo() onlyOwner {
...
}
下划线表示装饰函数的函数体应在何时执行。
总结一下,函数具有以下签名:
function doSomething() {external|internal}{public|private} [pure|view|payable] [modifiers] [returns ()]
<> = required
{} = recommended
[] = optional
signature = name + parameter types + function types +accessibility + default modifiers + custom modifiers + return types
// explicit function signatures allow for easy translation to ABI
按照限制由少到多的顺序总结函数类型与可访问性:
public: 所有人可访问
external: 仅能从外部访问
internal: 仅能被承继合约访问
private: 仅能从内部访问
函数参数由类型与名称定义。 名称不能为保留关键字。
function foo(uint a, uint b) {}
函数可以返回多个值。 返回多个值时,必须在函数签名中声明。 有两种方法定义返回参数:
类别带名称:
function foo() returns (uint a) {
a = 3; // returnstatement not necessary(不需返回语句)
}
类别不带名称:
function foo() returns (uint) {
return 3; // returnstatement required(需返回语句)
}
合约可以有一个匿名回退函数。 此函数不能有参数,也不能返回任何内容,但在被调用时可以提供负载。由于gas上限为2300,所以这个函数也做不了什么。合约调用指定某些不存在的函数或负载为空(即,普通ETH转账)时,执行回退。 此时,为使合约能够接受ETH,必须将回退函数标记为应付。
pragma solidity ^0.4.24;
contract Token {
function () payable {
}
}
虽然web3总能返回能更新状态的调用的交易哈希,但状态变更函数仍可以返回值供其他合约使用(即合约间消息调用)。 尽管如此,这些函数仍可以发出web3用交易哈希即可监视的事件。
同JavaScript的控制台函数,事件可用于记录。 被调用时,函数参数被存储在区块链上的特殊数据结构(也称交易日志)中。 可以在任何函数中发出事件,然后使用回调函数(callback)借由ABI进行监视。
示例事件:
pragma solidity ^0.4.24;
contract Token {
event Transfer(
address indexed_from,
address indexed _to,
uint256 _value
);
function transfer(address _to, uint256 _value) public {
...
emit Transfer(msg.sender,_to, _value);
}
}
示例callback:
const token = new web3.eth.Contract(ABI, ADDRESS);
const event = token.Transfer();
event.watch((error, result) => {
if (!error)console.log(result);
});
注意:快速查询时最多可以为3个事件参数建立索引。
总结一下,合约声明可以包含:
1. 状态变量
2. 参考变量:数组、构造、枚举、映射
3. 事件
4. 函数修饰符
5. 构造函数
6. 回退函数
7. 外部函数
8. 公共函数
9. 内部函数
10. 私有函数
// 理想、正常状态下应该是这个顺序
// 每个函数组中,状态变更函数应排在只读函数前(view在pure前)
// 变量与函数名称应为camelcase
同JavaScript,EVM的全局命名空间中始终存在特殊变量与函数。要访问有关交易或区块的信息,可以分别使用msg和block全局变量。
未完待续。