智能合约简介
简单的智能合约
让我们从一个基本示例开始,该示例设置一个变量的值,并将其公开供其他合同访问。如果你现在不理解所有的事情,那就好了,我们稍后会详细讨论。
存储示例
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
第一行告诉您源代码是在gplversion3.0下授权的。在默认发布源代码的设置中,机器可读的许可证说明符非常重要。
下一行指定源代码是为Solidity版本0.4.16或语言的更新版本(但不包括版本0.9.0)编写的。这是为了确保合约不能用新的(打破的)编译器版本编译,因为在新版本中它的行为可能会有所不同。 Pragmas 是编译器关于如何处理源代码的通用说明(例如 pragma once )。
从 Solidity 的意义上讲,契约是一组代码 功能 )和数据(ITS 状态 )位于以太坊区块链上的特定地址。这条线 uint storedData;
声明一个名为 storedData
类型的 uint
( u 已签名 int 第页,共页 256 位)。您可以将其视为数据库中的单个槽,您可以通过调用管理数据库的代码的函数进行查询和更改。在这个例子中,契约定义了函数 set
和 get
可用于修改或检索变量值的。
若要访问当前协定的成员(如状态变量),通常不会添加 this.
前缀,您只需通过其名称直接访问它。与其他一些语言不同,省略它不仅仅是一个风格问题,它会导致一种完全不同的访问成员的方式,稍后将对此进行详细介绍。
除了(由于以太坊建立的基础设施)允许任何人存储一个世界上任何人都可以访问的单一号码,而没有(可行的)阻止您发布此号码之外,本合同还没有做太多工作。任何人都可以打电话 set
再次以不同的值覆盖您的数字,但该数字仍存储在区块链的历史中。稍后,您将看到如何施加访问限制,以便只有您可以更改号码。
警告
注意使用Unicode文本,因为相似的(甚至相同的)字符可以有不同的代码点,因此编码为不同的字节数组。
注解
所有标识符(合同名、函数名和变量名)都限于ASCII字符集。可以将UTF-8编码的数据存储在字符串变量中。
Subcurrency 示例
下面的契约实现了加密货币的最简单形式。合同只允许创造者创造新的硬币(可能有不同的发行方案)。任何人都可以互相发送硬币,而不需要注册用户名和密码,您只需要一个以太坊密钥对。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping (address => uint) public balances;
// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);
// Constructor code is only run when the contract
// is created
constructor() {
minter = msg.sender;
}
// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
// Errors allow you to provide information about
// why an operation failed. They are returned
// to the caller of the function.
error InsufficientBalance(uint requested, uint available);
// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
这份合同介绍了一些新的概念,让我们逐一讨论一下。
线 address public minter;
声明类型为的状态变量 address . 这个 address
类型是不允许任何算术运算的160位值。它适用于存储合同地址,或属于 external accounts .
关键字 public
自动生成一个函数,该函数允许您从协定外部访问状态变量的当前值。如果没有此关键字,其他合同将无法访问该变量。编译器生成的函数的代码等同于以下代码(忽略 external
和 view
目前):
function minter() external view returns (address) { return minter; }
您可以自己添加一个类似于上述的函数,但是您将拥有一个同名的函数和状态变量。您不需要这样做,编译器会为您计算出来。
下一行, mapping (address => uint) public balances;
还创建了一个公共状态变量,但它是一个更复杂的数据类型。这个 mapping 键入将地址映射到 unsigned integers .
映射可以看作是 hash tables 它实际上是初始化的,这样每一个可能的键都从一开始就存在,并且被映射到一个字节表示都为零的值。但是,既不可能获得映射的所有键的列表,也不可能获得所有值的列表。记录添加到映射的内容,或者在不需要的上下文中使用它。或者更好的方法,保留一个列表,或者使用更合适的数据类型。
这个 getter function 由 public
关键字在映射的情况下更为复杂。它看起来如下所示:
function balances(address _account) external view returns (uint) {
return balances[_account];
}
您可以使用此功能查询单个账户的余额。
线 event Sent(address from, address to, uint amount);
声明一个 "event" ,在函数的最后一行中发出 send
.以太坊客户机(如Web应用程序)可以监听区块链上发出的这些事件,而无需付出太多成本。一旦发出,侦听器就接收到参数 from
, to
和 amount
从而可以跟踪事务。
要侦听此事件,可以使用以下javascript代码,该代码使用 web3.js 创建 Coin
合同对象,以及任何用户界面调用自动生成的 balances
从上面的函数:
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));
}
})
这个 constructor 是在创建合同期间执行的特殊函数,以后不能调用。在本例中,它永久存储创建合同的人的地址。这个 msg
变量(连同 tx
和 block
是一个 special global variable 包含允许访问区块链的属性。 msg.sender
始终是当前(外部)函数调用的来源地址。
构成合同的功能以及用户和合同可以调用的功能是 mint
和 send
.
The mint
function sends an amount of newly created coins to another address. The require function call defines conditions that reverts all changes if not met. In this
example, require(msg.sender == minter);
ensures that only the creator of the contract can call
mint
. In general, the creator can mint as many tokens as they like, but at some point, this will
lead to a phenomenon called "overflow". Note that because of the default Checked arithmetic, the transaction would revert if the expression balances[receiver] += amount;
overflows, i.e., when balances[receiver] + amount
in arbitrary precision arithmetic is larger
than the maximum value of uint
(2**256 - 1
). This is also true for the statement
balances[receiver] += amount;
in the function send
.
Errors 允许您向调用方提供有关条件或操作失败原因的更多信息。错误与 revert statement 。REVERT语句无条件中止并还原所有类似于 require
函数,但它还允许您提供错误名称和附加数据,这些数据将提供给调用方(并最终提供给前端应用程序或挡路浏览器),以便可以更轻松地调试故障或对故障做出反应。
这个 send
函数可以被任何人(已经拥有其中的一些硬币)用来将硬币发送给其他任何人。如果发件人没有足够的硬币可以发送, if
条件的计算结果为True。因此, revert
方法向发件人提供错误详细信息时,将导致操作失败。 InsufficientBalance
错误。
注解
如果您使用此合同将硬币发送到某个地址,则当您在区块链浏览器上查看该地址时,将看不到任何内容,因为您发送硬币的记录和更改的余额仅存储在该特定硬币合同的数据存储中。通过使用事件,您可以创建一个“区块链浏览器”,跟踪您的新硬币的交易和余额,但您必须检查硬币合同地址,而不是硬币所有者的地址。
区块链基础
对于程序员来说,区块链作为一个概念并不难理解。原因是大多数并发症(采矿, hashing , elliptic-curve cryptography , peer-to-peer networks 等等)只是为平台提供一组特定的特性和承诺。一旦你接受了这些特性,你就不必担心底层技术——或者你需要知道亚马逊的AWS是如何在内部工作的才能使用它?
交易
区块链是一个全球共享的交易数据库。这意味着每个人都可以通过参与网络来读取数据库中的条目。如果要更改数据库中的某些内容,则必须创建一个必须被所有其他人接受的所谓事务。“事务”一词意味着要进行的更改(假设您希望同时更改两个值)要么根本没有完成,要么完全应用。此外,当您的事务正在应用于数据库时,没有其他事务可以更改它。
例如,设想一个以电子货币列出所有账户余额的表。如果请求从一个帐户转移到另一个帐户,则数据库的事务性可以确保如果从一个帐户中减去金额,则总是将其添加到另一个帐户中。如果由于任何原因,无法将金额添加到目标帐户,则源帐户也不会被修改。
此外,事务总是由发送者(创建者)加密签名。这使得保护对数据库特定修改的访问变得简单。在电子货币的例子中,简单的支票确保只有持有账户钥匙的人才能从中转账。
阻碍
要克服的一个主要障碍是什么(用比特币的术语)被称为“双重消费攻击”:如果网络中存在两个同时想清空一个账户的交易,会发生什么?只有一个事务是有效的,通常是先被接受的事务。问题是,“第一个”不是对等网络中的一个客观术语。
抽象的答案是你不必在意。将为您选择全局接受的交易顺序,以解决冲突。这些事务将被捆绑到所谓的“块”中,然后在所有参与节点之间执行和分发。如果两个交易相互矛盾,最后成为第二个交易的交易将被拒绝,并不会成为该区块的一部分。
这些区块在时间上形成一个线性序列,这就是“区块链”一词的由来。区块以相当规则的间隔添加到链中-对于以太坊,大约每17秒一次。
作为“顺序选择机制”(称为“挖掘”)的一部分,可能会不时地恢复块,但只能在链的“尖端”恢复。在特定块上添加的块越多,恢复该块的可能性就越小。因此,你的交易可能会被恢复,甚至从区块链中删除,但是你等待的时间越长,它的可能性就越小。
注解
交易不保证包含在下一个块或任何特定的未来块中,因为它不是由交易的提交者决定的,而是由矿工决定包含在哪个块中的交易。
如果您想安排将来的合同通话,可以使用 alarm clock 或类似的Oracle服务。
以太坊虚拟机
概述
以太坊虚拟机或EVM是以太坊中智能合约的运行时环境。它不仅是沙盒,而且实际上是完全隔离的,这意味着EVM内部运行的代码无法访问网络、文件系统或其他进程。智能合约甚至限制了对其他智能合约的访问。
账户
以太坊中有两种账户共享同一地址空间: 外部账户 由公共-私有密钥对(即人类)控制的,以及 合同账户 它由与帐户一起存储的代码控制。
外部帐户的地址是从公钥确定的,而合同的地址是在合同创建时确定的(它是从创建者地址和从该地址发送的事务数派生的,即所谓的“nonce”)。
无论帐户是否存储代码,EVM都会平等地对待这两种类型。
每个帐户都有一个持久的键值存储映射256位字到256位字调用 存储 .
此外,每个账户都有一个 平衡 在乙醚中(确切地说,在“卫”中, 1 ether
是 10**18 wei
)可以通过发送包含ether的事务来修改。
交易
事务是从一个帐户发送到另一个帐户的消息(可能相同或为空,请参见下文)。它可以包括二进制数据(称为“有效载荷”)和 Ether 。
如果目标帐户包含代码,则执行该代码,并将有效负载作为输入数据提供。
如果未设置目标帐户(事务没有收件人或收件人设置为 null
)事务创建一个 新合同 .如前所述,该合同的地址不是零地址,而是从发送方获得的地址及其发送的事务数(“nonce”)。这种合同创建事务的有效负载被视为EVM字节码并执行。此执行的输出数据永久存储为合同代码。这意味着,为了创建合同,您不发送合同的实际代码,而是在执行时返回该代码的代码。
注解
创建合同时,其代码仍为空。因此,在正在构建的合同的构造函数完成执行之前,您不应该回调该合同。
气体
创建后,每个交易都会收取一定金额的 gas 其目的是限制执行事务所需的工作量,并同时为此执行支付费用。当EVM执行事务时,气体会根据特定规则逐渐耗尽。
这个 天然气价格 是由交易记录的创建者设置的值,必须支付 gas_price * gas
在发送帐户的前面。如果在执行后留下一些气体,它会以同样的方式退还给创造者。
如果气体在任何点耗尽(即为负),则会触发“耗尽气体”异常,该异常将恢复对当前调用帧中的状态所做的所有修改。
存储、内存和堆栈
以太坊虚拟机有三个可以存储数据的区域——存储、内存和堆栈,下面的段落将对此进行解释。
每个帐户都有一个名为 存储 ,在函数调用和事务之间保持不变。存储是一个将256位字映射到256位字的键值存储。从合同中枚举存储是不可能的,读取成本相对较高,初始化和修改存储的成本甚至更高。由于这一成本,您应该将持久性存储中存储的内容减至合同需要运行的内容。在合同之外存储派生计算、缓存和聚合等数据。合同除了自己的以外,不能读也不能写任何存储器。
第二个数据区域称为 记忆 ,其中一个协定为每个消息调用获取一个新清除的实例。内存是线性的,可以在字节级寻址,但读操作的宽度限制为256位,而写操作的宽度可以是8位或256位。当访问(读或写)一个以前未接触的内存字(即一个字内的任何偏移量)时,内存被扩展一个字(256位)。在扩建时,必须支付天然气成本。内存越大,成本越高(按四次方缩放)。
EVM不是寄存器机器,而是堆栈机器,因此所有计算都是在名为 堆栈 .它的最大大小为1024个元素,包含256位的字。对堆栈的访问仅限于顶端,方法如下:可以将最顶端的16个元素之一复制到堆栈顶部,或者将最顶端的元素与下面的16个元素之一交换。所有其他操作从堆栈中获取最顶部的两个(或一个或多个,具体取决于操作)元素,并将结果推送到堆栈中。当然,为了更深入地访问堆栈,可以将堆栈元素移动到存储或内存中,但不可能在不首先删除堆栈顶部的情况下,只访问堆栈中更深的任意元素。
指令集
EVM的指令集保持最小,以避免错误或不一致的实现,从而导致共识问题。所有指令都在基本数据类型、256位字或内存片(或其他字节数组)上操作。通常的算术运算、位运算、逻辑运算和比较运算都会出现。有条件和无条件跳转是可能的。此外,契约可以访问当前块的相关属性,如其编号和时间戳。
有关完整列表,请参阅 list of opcodes 作为内联汇编文档的一部分。
消息呼叫
合同可以通过消息调用的方式调用其他合同或向非合同帐户发送 Ether 。消息调用与事务类似,因为它们具有源、目标、数据有效负载、 Ether 、气体和返回数据。事实上,每个事务都由一个顶级消息调用组成,这反过来又可以创建更多的消息调用。
合同可以决定剩下多少 gas 应该与内部消息调用一起发送,以及它希望保留多少内容。如果在内部调用(或任何其他异常)中发生气体不足异常,则将通过放入堆栈的错误值来发出此信号。在这种情况下,只有与呼叫一起发送的气体用完。在solidity中,调用契约在这种情况下默认会导致手动异常,从而使异常“冒泡”调用堆栈。
如前所述,被调用的契约(可以与调用方相同)将接收一个新清除的内存实例,并可以访问调用有效负载,该有效负载将在称为 呼叫数据 .完成执行后,它可以返回数据,这些数据将存储在调用者预先分配的调用者内存中的某个位置。所有这些调用都是完全同步的。
电话是 有限的 深度为1024,这意味着对于更复杂的操作,循环应该优先于递归调用。此外,在消息调用中只能转发63/64的气体,这在实践中导致深度限制略小于1000。
代表呼叫/呼叫代码和库
存在一个特殊的消息调用变量,名为 委托人电话 它与消息调用相同,除了目标地址的代码是在调用合同的上下文中执行的, msg.sender
和 msg.value
不要更改它们的值。
这意味着契约可以在运行时从不同的地址动态加载代码。存储、当前地址和余额仍指调用合同,只有代码取自被调用地址。
这使得实现“库”功能成为可能:可重用的库代码,可应用于合同的存储,例如为了实现复杂的数据结构。
日志
可以将数据存储在一个特殊的索引数据结构中,该结构将一直映射到块级别。此功能调用 logs 由solidity用于实现 events .合同在创建后无法访问日志数据,但可以从区块链外部有效访问。因为日志数据的某些部分存储在 bloom filters ,以一种高效且加密安全的方式搜索这些数据是可能的,因此不下载整个区块链的网络对等方(所谓的“轻客户端”)仍然可以找到这些日志。
创造
契约甚至可以使用一个特殊的操作码创建其他契约(即它们不会像事务那样简单地调用零地址)。它们之间唯一的区别 创建呼叫 正常的消息调用是执行有效负载数据,结果存储为代码,调用方/创建者在堆栈上接收新契约的地址。
停用和自毁
从区块链中删除代码的唯一方法是当位于该地址的合同执行 selfdestruct
操作。存储在该地址的其余 Ether 被发送到指定目标,然后存储和代码从状态中删除。从理论上讲,删除合同似乎是一个好主意,但这有潜在的危险,就好像有人发送以太去删除合同,以太永远都会丢失。
警告
即使合同被 selfdestruct
,它仍然是区块链历史的一部分,可能被大多数以太坊节点保留。所以使用 selfdestruct
与从硬盘上删除数据不同。
注解
即使合同代码不包含调用 selfdestruct
,它仍然可以使用 delegatecall
或 callcode
.
如果你想取消你的合同,你应该改为 使残废 它们通过改变一些内部状态来恢复所有的函数。这使得合同无法使用,因为它立即返回 Ether 。