2.1 Introduction to Smart Contra
Introduction to Smart Contracts
A Simple Smart Contract
让我们从一个基本示例开始,该示例设置变量的值并将其公开以供其他合约访问。如果现在不了解所有内容,这很好,我们稍后会详细介绍。
Storage
pragma solidity >=0.4.0 <0.6.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
第一行简单地告诉源代码是为Solidity版本0.4.0或任何不破坏功能的更新版本(最高版本,但不包括版本0.6.0)。这是为了确保合约不能与新的(破碎的)编译器版本兼容,它可能表现不同。所谓的编译指示是编译器关于如何处理源代码的常见指令(例如,pragma once)。
Solidity意义上的合约是代码(其函数)和数据(其状态)的集合,它位于以太坊区块链的特定地址。行 uint storedData;
声明一个名为 storedData
的状态变量,类型为 uint
(u * nsigned * int * eger为* 256位)。可以将其视为数据库中的单个插槽,可以通过调用管理数据库的代码的函数来查询和更改它。在以太坊的情况下,这始终是拥有合约。在这种情况下,函数 set
和 get
可用于修改或检索变量的值。
要访问状态变量,不需要前缀 this
,这在其他语言中很常见。
除了(由于以太坊建立的基础设施)之外,该合约还没有做太多工作,允许任何人存储世界上任何人都可以访问的单个号码,而没有(可行的)方法来阻止发布此号码。当然,任何人都可以使用不同的值再次调用 set
并覆盖我们的号码,但该号码仍会存储在区块链的历史记录中。稍后,我们将看到如何施加访问限制,以便只有我们可以更改数量。
Note
所有标识符(合约名称,函数名称和变量名称)都限制为ASCII字符集。可以将UTF-8编码数据存储在字符串变量中。
Warning
小心使用Unicode文本,因为类似的(甚至相同的)字符可以具有不同的代码点,因此将被编码为不同的字节数组。
Subcurrency Example
以下合约将实现最简单的加密货币形式。可以凭空创造代币,但只有创建合约的人才能做到这一点(很容易实施不同的发行方案)。此外,任何人都可以相互发送代币而无需使用用户名和密码进行注册 - 只需要一个以太坊密钥对。
pragma solidity >0.4.99 <0.6.0;
contract Coin {
// The keyword "public" makes those variables
// easily readable from outside.
address public minter;
mapping (address => uint) public balances;
// Events allow light clients to react to
// changes efficiently.
event Sent(address from, address to, uint amount);
// This is the constructor whose code is
// run only when the contract is created.
constructor() public {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
require(amount < 1e60);
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
本合约介绍了一些新概念,让我们逐一介绍。
行 address public minter;
声明可公开访问的类型为address的状态变量。address
类型是160位值,不允许任何算术运算。它适用于存储合约或属于外部人员的密钥对的地址。关键字 public
会自动生成一个函数,允许从合约外部访问状态变量的当前值。没有此关键字,其他合约无法访问该变量。编译器生成的函数代码大致等同于以下内容(暂时忽略external
和 view
):
function minter() external view returns (address) { return minter; }
当然,添加一个完全相同的函数是行不通的,因为我们会有一个函数和一个具有相同名称的状态变量,但希望你能得到这个想法 - 编译器会为你找出答案。
下一行,mapping (address => uint) public balances;
还会创建一个公共状态变量,但它是一种更复杂的数据类型。类型映射地址为无符号整数。映射可以看作是哈希表,它们被虚拟地初始化,使得每个可能的键从一开始就存在并被映射到其字节表示全为零的值。但是,这种类比并不会太过分,因为既不可能获得映射的所有键列表,也不可能获得所有值的列表。因此,请记住(或更好地保留列表或使用更高级的数据类型)添加到映射中的内容或在不需要此上下文的上下文中使用它。在这种情况下,public
关键字创建的 getter function
有点复杂。它大致如下所示:
function balances(address _account) external view returns (uint) {
return balances[_account];
}
如您所见,您可以使用此功能轻松查询单个帐户的余额。
行 event Sent(address from, address to, uint amount);
声明一个所谓的“事件”,它在函数 send
的最后一行发出。用户接口(当然还有服务器应用程序)可以在没有太多成本的情况下监听区块链上发出的事件。一旦发出,侦听器也将接收 from
,to
和 amount
的参数,这使得跟踪交易变得容易。为了侦听此事件,您将使用以下JavaScript代码(假设 Coin
是通过web3.js或类似模块创建的合约对象):
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
请注意如何从用户接口调用自动生成的函数 balances
。
构造函数是一个特殊函数,它在创建合约期间运行,之后不能调用。它永久存储创建合约的人的地址:msg
(与 tx
和 block
一起)是一个特殊的全局变量,它包含一些允许访问区块链的属性。 msg.sender
始终是当前(外部)函数调用的来源地址。
最后,实际上以合约结束并且可以被用户和合约调用的功能都类似 mint
和 send
。如果除了创建合约的帐户以外的任何人调用 mint
,则不会发生任何事情。这是由特殊函数 require
确保的,如果其参数的计算结果为 false
,则会导致所有更改都被还原。第二个 require
确保不会有太多代币,这可能会导致以后出现溢出错误。
另一方面,任何人(已经有一些这些代币)都可以使用 send
发送代币给其他人。如果您没有足够的代币发送,则 require
调用将失败,并为用户提供相应的错误消息字符串。
Note
如果您使用此合约将代币发送到地址,当您在区块链资源管理器中查看该地址时,您将看不到任何内容,因为您发送硬币和更改的余额的事实仅存储在此特定代币合约的数据存储中。通过使用事件,创建一个跟踪新代币交易和余额的“区块链资源管理器”相对容易,但您必须检查代币合约地址而不是代币所有者的地址。
Blockchain Basics
区块链作为一个概念对程序员来说并不难理解。原因是大多数复杂性(挖掘,散列,椭圆曲线加密,点对点网络等)只是为平台提供一系列功能和承诺。一旦您接受了给定的这些功能,您就不必担心底层技术 - 或者您是否必须知道Amazon的AWS如何在内部运行才能使用它?
Transactions
区块链是一个全局共享的交易数据库。这意味着每个人都可以通过参与网络来读取数据库中的条目。如果要更改数据库中的某些内容,则必须创建一个必须被所有其他人接受的所谓交易。单词交易意味着您要进行的更改(假设您想要同时更改两个值)要么根本没有完成,要么完全应用。此外,当您的交易应用于数据库时,没有其他交易可以改变它。
例如,假设一个表格列出了电子货币中所有账户的余额。如果请求从一个帐户转移到另一个帐户,则数据库的事务性质确保如果从一个帐户中减去该金额,则始终将其添加到另一个帐户。如果由于某种原因,无法将金额添加到目标帐户,则也不会修改源帐户。
此外,交易总是由发起人(创建者)以加密方式签名。这使得可以直接保护对数据库的特定修改的访问。在电子货币的示例中,简单的检查确保只有持有账户密钥的人才能从中转账。
Blocks
要克服的一个主要障碍是(比特币术语)被称为“双重花费攻击”:如果网络中存在两个想要清算帐户的交易,会发生什么?只有一个交易可以有效,通常是首先接受的交易。问题是“第一”不是端到端网络中的客观术语。
对此的抽象答案是您不必关心。将为您选择全局接受的交易顺序,以解决冲突。交易将捆绑到所谓的“区块”中,然后它们将在所有参与节点之间执行和分发。如果两个交易相互矛盾,那么最终成为第二个的交易将被拒绝并且不会成为该块的一部分。
这些区块在时间上形成线性序列,这是“区块链”一词源自的地方。区块以相当规则的间隔添加到链中 - 对于以太坊,这大约每17秒。
作为“订单选择机制”(称为“挖矿”)的一部分,可能会发生区块不时被恢复,但仅在链的 "tip" 处。在特定区块的顶部添加的区块越多,该区块恢复的可能性就越小。因此,您的交易可能会被还原甚至从区块链中删除,但等待的时间越长,它的可能性就越小。
Note
交易不保证包含在下一个区块或任何特定的未来区块中,因为它不取决于交易的提交者,而是取决于矿工以确定交易包含在哪个块中。
如果您想安排未来的合约通话,您可以使用 [alarm clock](https://www.ethereum-alarm-clock.com/) 或类似的oracle服务。
The Ethereum Virtual Machine
Overview
以太坊虚拟机或Ethereum Virtual Machine, EVM是以太坊中智能合约的运行时环境。它不仅是沙箱,而且实际上是完全隔离的,这意味着在EVM内部运行的代码无法访问网络,文件系统或其他进程。智能合约甚至被限制访问其它的智能合约。
Accounts
在以太坊中有两种帐户共享相同的地址空间:由公钥-私钥对(即人)控制的外部帐户(External accounts)和由与帐户一起存储的代码控制的合约帐户(contracts accounts)。
外部帐户的地址是根据公钥确定的,而合约的地址是在创建合约时确定的(它来自创建者地址和从该地址发送的交易数量,即所谓的 "nonce")。
无论帐户是否存储代码,EVM都会平等对待这两种类型。
每个帐户都有一个持久的键值存储,将256位字映射到称为存储(storage)的256位字。
此外,每个帐户在以太网中都有余额(balance)(准确地说是“Wei”,1个以太币是10 ** 18 wei),可以通过发送包含以太币的交易来修改。
Transactions
交易是从一个帐户发送到另一个帐户的消息(可能相同或为空,见下文)。它可以包括二进制数据(称为 "payload")和以太币。
如果目标帐户包含代码,则执行该代码并将有效负载作为输入数据提供。
如果未设置目标帐户(交易没有收件人或收件人设置为 null
),则交易将创建新合约。如前所述,该合约的地址不是零地址,而是从发送方及其发送的交易数量("nonce")得出的地址。这种合约创建交易的有效载荷(payload)被认为是EVM字节码并被执行。此执行的输出数据将永久存储为合约代码。这意味着,为了创建合约,您不会发送合约的实际代码,而是实际上在执行时返回该代码的代码。
Note
在创建合约时,其代码仍为空。因此,在构造函数执行完毕之前,不应回调合约中的其它函数。
Gas
在创建时,每笔交易都收取一定数量的 gas,其目的是限制执行交易所需的工作量并同时支付该执行。当EVM执行交易时,根据特定规则逐渐耗尽 gas。
gas 价格是交易创建者设定的价值,他必须从发送账户预先支付 gas_price * gas
。如果在执行后遗留了一些 gas,它将以相同的方式退还给创建者。
如果 gas 在任何点用完(即它将是负的),则触发 gas 不足异常,这将恢复对当前呼叫帧中的状态所做的所有修改。
Storage, Memory and the Stack
以太坊虚拟机有三个区域,可以存储数据存储,内存和堆栈,这些将在以下段落中介绍。
每个帐户都有一个名为 storage
的数据区,它在函数调用和交易之间是持久的。存储是一个键值存储,可将256位字映射到256位字。不可能从合约中枚举存储,并且读取成本相对较高,甚至更难以修改存储。合约既不能读取也不能写入任何存储,除了它自己的存储。
第二个数据区域称为内存,其中一个合约为每个消息调用获取一个新的实例。存储器是线性的,可以在字节级寻址,但读取限制为256位宽,而写操作可以是8位或256位宽。当访问(读取或写入)先前未触及的存储器字(即,字内的任何偏移)时,存储器通过字(256位)扩展。在扩张时,必须支付 gas 费用。随着内存的增大,内存成本越高(它以二次方式缩放)。
EVM不是寄存器机器而是堆栈机器,因此所有计算都在称为堆栈的数据区域上执行。它的最大大小为1024个元素,包含256位的字。通过以下方式访问堆栈仅限于顶端:可以将最顶部的16个元素之一复制到堆栈的顶部,或者将最顶层的元素与其下面的16个元素之一交换。所有其他操作从堆栈中取最顶部的两个(或一个或多个,取决于操作)元素并将结果推送到堆栈。当然,可以将堆栈元素移动到存储或内存以便更深入地访问堆栈,但是如果不首先移除堆栈的顶部,就不可能只访问堆栈中更深的任意元素。
Instruction Set
EVM的指令集保持最小,以避免可能导致共识问题的不正确或不一致的实现。所有指令都在基本数据类型,256位字或内存片(或其他字节数组)上运行。存在通常的算术,位,逻辑和比较操作。有条件和无条件跳转是可能的。此外,合约可以访问当前块的相关属性,如其编号和时间戳。
有关完整列表,请参阅内联汇编文档中的操作码列表。
Message Calls
合约可以通过消息调用方式调用其他合约或将Ether发送到非合约帐户。消息调用类似于交易,因为它们具有源,目标,数据有效负载,以太币,gas 和返回数据。实际上,每个交易都包含一个顶级消息调用,而这反过来又可以创建进一步的消息调用。
合约可以决定应该通过内部消息调用发送多少剩余的 gas 以及它想要保留多少。如果内部调用(或任何其他异常)中发生了 gas 异常,则将通过放入堆栈的错误值来发出信号。在这种情况下,只有与 call 一起发送的 gas 用完。在Solidity中,调用合约在这种情况下默认会导致手动异常,因此异常会 "bullble up" 调用堆栈。
如前所述,被叫合约(可以与调用者相同)将接收新近清除的内存实例,并且可以访问调用有效负载 - 这将在称为calldata的单独区域中提供。完成执行后,它可以返回数据,这些数据将存储在调用者预先分配的调用者内存中的某个位置。所有这些呼叫都完全同步。
调用的深度限制为1024,这意味着对于更复杂的操作,循环应优先于递归调用。此外,在消息调用中只能转发63/64的 gas,这在实践中导致深度限制略小于1000。
Delegatecall / Callcode and Libraries
存在一个特殊的消息调用变体,名为delegatecall,它与消息调用相同,除了目标地址的代码在调用合约的上下文中执行而msg.sender
和 msg.value
不改变他们的 values。
这意味着合约可以在运行时从不同的地址动态加载代码。存储,当前地址和余额仍然是指调用合约,只有代码来自被调用地址。
这使得可以在Solidity:Reusable库代码中实现“库”功能,该库代码可以应用于合约的存储,例如,为了实现复杂的数据结构。
Logs
可以将数据存储在特殊索引的数据结构中,该数据结构一直映射到块级别。 Solidity使用此功能称为日志来实现事件。合约在创建后无法访问日志数据,但可以从区块链外部有效地访问它们。由于日志数据的某些部分存储在 bloom过滤器 中,因此可以以高效且加密的方式搜索此数据,因此不下载整个区块链的网络对等体(所谓的“轻型客户端”)仍然可以找到这些日志。
Create
合约甚至可以使用特殊操作码创建其他合约(即,他们不会简单地将零地址称为交易)。这些创建调用和普通消息调用之间的唯一区别是执行有效负载数据,结果存储为代码,调用者/创建者接收堆栈上新合约的地址。
Deactivate and Self-destruct
从区块链中删除代码的唯一方法是在该地址的合约执行自毁构造操作时。存储在该地址的剩余以太网将发送到指定目标,然后从该状态中删除存储和代码。理论上删除合同听起来是个好主意,但它有潜在的危险,就好像有人发送Ether来删除合同一样,Ether永远丢失了。
Note
即使合约的代码不包含对 `selfdestruct` 的调用,它仍然可以使用 `delegatecall` 或 `callcode` 执行该操作。
如果要取消激活合约,则应通过更改某些导致所有函数还原的内部状态来禁用它们。这使得无法使用合约,因为它立即返回以太币。
Warning
即使通过 `selfdestruct` 删除合约,它仍然是区块链历史的一部分,并且可能由大多数以太坊节点保留。因此,使用`selfdestruct` 与从硬盘删除数据不同。
注解
旧合约的删减可能会,也可能不会被以太坊的各种客户端程序实现。另外,归档节点可选择无限期保留合约存储和代码。
注解
目前, **外部账户** 不能从状态中移除。
项目源代码
项目源代码会逐步上传到 Github,地址为 https://github.com/windstamp/dapp。
Contributor
- Windstamp, https://github.com/windstamp