主页 > imtoken钱包下载注册教程 > Solidity 文档(中文版)系列一:智能合约概述

Solidity 文档(中文版)系列一:智能合约概述

imtoken钱包下载注册教程 2023-05-21 06:10:16

连载前言:

从本文开始,我将连载中文版的Solidity文档(以太坊官方Solidity开发手册)。 本文档的英文原文可在以太坊官网底部的Solidity链接中找到。 官方英文版文档中有中文翻译链接,是本次连载内容的出处。 本次连载将按照英文文档的先后顺序进行。

Solidity 是以太坊官方用于智能合约开发的高级语言。 本中文翻译由Hiblock社区组织贡献,官方Github:。

我本人是三月初加入项目的,目前以管理员、贡献者、编辑的身份参与日常工作; 截至4月底,大部分翻译工作已经完成。 感兴趣的朋友请直接在以太坊官网链接查看最新中文版本状态,或关注上述中文翻译的Github仓库。

为了单独阅读的需要,我在连载中删除了原文中的第一个控制标签、外部链接和内部链接。

本文作为Solidity文档的开篇,介绍了以太坊智能合约相关的几乎所有基本概念。 学习以太坊智能合约的开发对我们来说非常重要,也是初学者不可忽视的一章。 缺乏对基本概念的理解,会给以后的学习和编码带来困难,初学者一定要注意。

智能合约概述 简单的智能合约

我们先来看最基本的例子。 如果您现在不理解也没关系,我们稍后会更深入地解释它。

店铺

pragma solidity ^0.4.0;
contract SimpleStorage {
    uint storedData;
    function set(uint x) public {
        storedData = x;
    }
    function get() public constant returns (uint) {
        return storedData;
    }
}

以太坊智能合约自动执行_sitehqz.com 以太坊 智能合约_以太坊智能合约的众筹

第一行是告诉大家源码是用Solidity 0.4.0版本写的,用0.4.0以上版本运行没有问题(最高0.5.0,但不包括0.5.0)。 这是为了确保合约不会在新的编译器版本中突然出现异常。 通常,称为编译指示的关键字是告诉编译器如何处理源代码的指令。

Solidity 中合约的含义是一组代码(它的功能)和数据(它的状态),它们驻留在以太坊区块链上的特定地址。 该行 uint storedData; 声明一个名为 storedData 的 uint(256 位无符号整数)类型的状态变量。 您可以将其视为数据库中的一个位置,可以通过调用管理数据库代码的函数来查询和更改该位置。 对于以太坊,上述合约是拥有合约。 在这种情况下,函数 set 和 get 可用于更改或检索变量的值。

要访问状态变量,需要这样的前缀。 不是必需的,尽管这在其他语言中是常​​见的做法。

这个合约能做的事情不多(由于以太坊所建立的基础设施):它允许任何人在合约中存储一个数字,世界上任何人都可以访问这个数字,而没有可行的方法阻止你从发布号码。 当然以太坊智能合约自动执行,任何人都可以再次调用 set,传入不同的值,覆盖你的号码,但该号码仍将存储在区块链的历史记录中。 稍后,我们将了解如何施加访问限制以确保只有您可以更改此号码。

注意:所有标识符(合约名称、函数名称和变量名称)只能使用 ASCII 字符集。 UTF-8编码的数据可以以字符串变量的形式存储。

警告:小心处理 Unicode 文本,因为有些字符看起来很相似(甚至相同),但具有不同的字符编码值,它们的编码字符数组也会不同。

子货币示例

下面的合约实现了最简单的加密货币。 在这里,Coin(硬币)确实可以无中生有,但只有创建合约的人才能做到(实施不同的发行计划并不难)。 此外,任何人都可以将硬币转移给其他人,不需要注册用户名和密码——只需要一个以太坊密钥对。

pragma solidity ^0.4.21;
contract Coin {
    // 关键字“public”让这些变量可以从外部读取
    address public minter;
    mapping (address => uint) public balances;
    // 轻客户端可以通过事件针对变化作出高效的反应
    event Sent(address from, address to, uint amount);
    // 这是构造函数,只有当合约创建时运行
    function Coin() public {
        minter = msg.sender;
    }
    function mint(address receiver, uint amount) public {

sitehqz.com 以太坊 智能合约_以太坊智能合约自动执行_以太坊智能合约的众筹

if (msg.sender != minter) return; balances[receiver] += amount; } function send(address receiver, uint amount) public { if (balances[msg.sender] < amount) return; balances[msg.sender] -= amount; balances[receiver] += amount; emit Sent(msg.sender, receiver, amount); } }

这个合约引入了一些新的概念,我们来一一分解。

解决公共铸造者; 这一行声明了一个可以公开访问的地址类型的状态变量。 地址类型是一个 160 位的值,不允许任何算术运算。 这种类型适用于存储外部方的合约地址或密钥对。 关键字 public 自动生成一个函数,允许您在该合约之外访问该状态变量的当前值。 如果没有 this 关键字,其他合约将无法访问该变量。 编译器生成的函数代码大致如下所示:

    function minter() returns (address) { return minter; }

当然,添加一个与上面完全相同的函数是行不通的,因为我们将有一个函数和一个同名的变量。 在这里,主要是希望你能明白——编译器已经帮你实现了。

下一行,mapping (address => uint) public balances; 还创建了一个公共状态变量,但它是一种更复杂的数据类型。 此类型将地址映射到无符号整数。 映射可以被认为是执行虚拟初始化的哈希表,以便所有可能的键映射到一个字节表示全为零的值。 然而,这个类比不太合适,因为我们既不能得到所有键的列表,也不能得到一个映射的所有值。 因此,要么记住您添加到映射中的数据(最好使用列表或更高级别的数据类型),要么在不需要键列表或值列表的上下文中使用它,如本例所示。 public关键字创建的getter函数是比较复杂的情况,大致长这样:

    function balances(address _account) public view returns (uint) {
        return balances[_account];
    }

可以看到,通过这个功能可以很方便的查询到账户的余额。

以太坊智能合约的众筹_以太坊智能合约自动执行_sitehqz.com 以太坊 智能合约

事件发送(地址从,地址到,单位数量); 这一行声明了一个所谓的“事件”,它将在发送函数的最后一行发出。 用户界面(当然还有服务器应用程序)可以无需太多成本即可监听区块链上产生的事件。 一旦发出,所有监听该事件的监听器都会收到通知。 并且所有事件都包含from、to和amount三个参数,可以方便跟踪交易。 要侦听此事件,您可以使用以下代码:

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));
    }
})

请注意此处如何从用户界面调用自动生成的余额函数。

特殊函数 Coin 是在合约创建期间执行的构造函数,之后无法调用。 它永久存储创建合约的人的地址:msg(以及 tx 和 block)是一个神奇的全局变量,包含一些允许访问区块链的属性。 msg.sender 始终是当前(外部)函数调用的源地址。

最后,真正被用户或其他合约调用来完成本合约功能的方法是 mint 和 send。 如果 mint 被合约创建者以外的人调用,则什么也不会发生。 另一方面,任何人都可以使用发送功能将硬币发送给其他任何人(当然,前提是发送者拥有硬币)。 请记住,如果您使用合约将硬币发送到某个地址,当您在区块链浏览器上查看该地址时,您将看不到任何关于该地址的信息。 因为,实际上,你发币和改变余额的信息,只存储在具体合约的数据存储区中。 通过使用事件,您可以非常简单地为您的新硬币创建一个“区块链浏览器”来跟踪交易和余额。

区块链基础

对于程序员来说,区块链的概念并不难理解,因为大多数难懂的东西(如挖矿、哈希、椭圆曲线密码学、点对点(P2P)网络等)都提供了一些特定的功能和承诺。 你只需要接受这些已有的特性,不必关心底层技术。 比如亚马逊的AWS,你是不是要知道内部原理,才能用?

贸易

区块链是一个全球共享的交易数据库,这意味着任何加入网络的人都可以读取数据库中的记录。 如果你想改变数据库中的某些东西,你必须创建一个被其他人接受的事务。 事务这个词的意思是你想做什么(假设你想同时改变两个值),要么什么都不做,要么全部做。 此外,当您的事务应用于数据库时,其他事务不能修改数据库。

举个例子,假设有一张表格列出了电子货币中所有账户的余额。 如果请求从一个账户转账到另一个账户,数据库的交易性质确保如果从一个账户中扣除一笔金额,它总是会添加到另一个账户中。 如果由于某种原因无法将金额添加到目标帐户,则源帐户不会发生任何事情。

此外,交易总是由发送者(创建者)签名。

这样,就可以很简单的为数据库的特定修改添加访问保护机制。 就电子货币而言,一张简单的支票可确保只有持有账户密钥的人才能从中转账。

堵塞

sitehqz.com 以太坊 智能合约_以太坊智能合约自动执行_以太坊智能合约的众筹

在比特币中,要解决的主要问题之一被称为“双花攻击”:如果网络上有两笔交易都试图从同一个账户花钱,会发生什么? 所谓的冲突?

简短的回答是你不必关心这个问题。 网络自动为您选择交易顺序并将它们打包成所谓的“块”,然后执行并分发给所有参与节点。 如果两个交易相互矛盾,则后面的交易将被拒绝并且不包含在块中。

这些区块按时间顺序形成一个线性序列,这就是“区块链”一词的由来。 区块会定期添加到链中——对于以太坊来说,这个间隔大约是 17 秒。

作为“顺序选择机制”(也称为“挖掘”)的一部分,有时可能会发生块被回滚的情况,但仅限于链的“末端”。 最后添加的块越多,回滚的概率越低。 因此,您的交易有可能被回滚甚至从区块链中删除,但您等待的时间越长,这种情况发生的可能性就越小。

以太坊虚拟机概述

以太坊虚拟机(或 EVM)是智能合约的运行环境。 它不仅被沙箱封装,而且完全隔离,这意味着运行在 EVM 中的代码无法访问网络、文件系统和其他进程。 甚至智能合约之间的访问也受到限制。

帐户

以太坊中有两种账户(它们共享相同的地址空间):外部账户由公私钥对(即人)控制; 合约账户由存储在账户中的代码控制。

外部账户的地址由公钥确定,而合约账户的地址是在合约创建时确定的(该地址由合约创建者的地址和从该地址发送的交易数量计算得出,即就是所谓的“nonce”)。

无论帐户是否存储代码,这两种类型的帐户在 EVM 看来都是一样的。

每个账户都有一个键值对形式的持久化存储区。 其中key和value的长度都是256位,我们称之为存储。

此外,每个账户都有一个以太币余额(balance)(以“Wei”为单位),它会随着包含以太币的交易发送而变化。

贸易

一笔交易可以看作是一条消息从一个账户发送到另一个账户(这里的账户,可以是同一个账户,也可以是特殊的零账户,见下文)。 它可以包含二进制数据(它的有效载荷)和以太币。

如果目标帐户包含代码,则此代码将以有效负载作为输入参数执行。

如果目标账户是零账户(账户地址为0),本次交易将创建一个新合约。 如上所述,合约的地址不是零地址,而是根据合约创建者的地址和从该地址发送的交易数量(即所谓的“nonce”)计算得出的。 用于创建合约的交易负载被转换为 EVM 字节码并被执行。 执行的输出将作为合约代码永久存储。 这意味着为了创建一个合约,你不需要将实际的合约代码发送给合约,而是发送产生实际代码的代码。

注意:在合约创建过程中,合约的代码是不存在的。 因此,在合约构造完成之前,您不应该从构造中调用合约的任何函数。

气体

一旦创建,每笔交易都会收取一定数量的天然气,以限制执行交易所需的工作量并支付交易费用。 当EVM执行一笔交易时,gas会按照一定的规则逐渐耗尽。

gas price是交易发送方设定的一个值,发送方账户需要预付的交易费用为gas_price * gas。 如果交易执行后还有剩余的gas,gas将以同样的方式返还。

无论在哪里执行,一旦gas耗尽(即减为负值),都会触发out-of-gas异常。 当前调用框架所做的所有状态修改都将被撤消。

译者注:call frame指的是下面提到的EVM运行栈(stack)中当前运行所需要的几个元素。

存储、内存和堆栈

以太坊智能合约自动执行_以太坊智能合约的众筹_sitehqz.com 以太坊 智能合约

每个帐户都有一个称为存储的持久内存区域。 存储是一个键值存储,将 256 位字(键)映射到 256 位字(值)。 (译者注,word是EVM数据处理的最小单位。)合约中不可能枚举存储; 读取存储的开销很高,修改存储的开销更高。 合约只能读写自己的部分存储区。

第二种内存区域称为内存,合约每次消息调用前都会获得一个干净的内存实例。 内存是线性的,可以按字节寻址,但读取数据的长度限制为 256 位,而写入的长度可以是 8 位或 256 位。 当访问(无论是读取还是写入)之前从未访问过的内存字(无论是在该字内的任何位置偏移)时,内存将按字扩展(每个字为 256 位)。 做这种扩张会消耗一定的gas。 随着内存使用量的增长,其成本(平方)也随之增长。

EVM不是基于寄存器的,而是基于栈的,所有的计算都在一个叫做栈(stack)的区域进行。 堆栈中最多可以有 1024 个元素,每个元素的长度为一个字(256 位)。 对栈的访问仅限于其栈顶,限制方法为:允许将栈顶16个元素之一复制到栈顶,或者将栈顶元素与下方16个元素之一交换它。 所有其他操作只能取栈顶的两个(或一个,或多个,视具体操作而定)元素,操作完成后,将结果压入栈顶。 当然,你可以把栈上的元素放到storage或者memory中。 但是如果不先从栈顶移除其他元素,就不可能只访问栈中任意深度的一个元素。

指令系统

EVM 的指令集设计得尽可能精简,以最大限度地减少可能导致共识问题的错误实现的可能性。 所有指令都对“256位字(word)”的基本数据类型进行操作。 常用的算术运算、位运算、逻辑运算和比较运算都支持。 还支持有条件和无条件跳转。 此外,合约还可以访问当前区块的相关属性,例如区块编号和时间戳。

留言电话

合约可以通过消息调用调用其他合约或发送以太币给非合约账户。 消息调用与交易非常相似,它们都有源、目的地、有效负载数据、以太币、gas 和返回数据。 事实上,每个交易都包含一个顶级消息调用,这反过来会创建更多的消息调用。

合约可以在其内部消息调用中决定它应该发送和保留多少剩余的气体。 如果内部消息调用发生 out-of-gas 异常(或任何其他异常),这将由压入堆栈的错误值指示。 此时以太坊智能合约自动执行,只会消耗内部消息调用发送的gas。 在 Solidity 中,调用合约默认会触发手动异常,以便异常可以从调用堆栈中“冒泡”。

如前所述,被调用的合约(可以是与调用者相同的合约)获得一个新清理的内存并可以访问调用有效负载——由一个名为 calldata 的单独区域提供的数据。 调用执行后,返回的数据会存放在调用者预先分配的一块内存中。

调用深度限制为 1024,因此对于更复杂的操作,我们应该使用循环而不是递归。

委托调用/代码调用和库

有一种特殊类型的消息调用称为委托调用。 它与一般消息调用的区别在于,目标地址的代码将在调用合约的上下文中执行,msg.sender 和 msg.value 保持不变。

这意味着合约可以在运行时从另一个地址动态加载代码。 存储、当前地址和余额都指向调用合约,只有代码是从调用地址中获取的。

这使得 Solidity 可以实现“库”特性:可以将可重用的代码库放在合约存储上,例如用于实现复杂数据结构的库。

日志

有一种特殊的可索引数据结构,可以将数据一直存储到块级别。 这个特性叫做日志,Solidity 用它来实现事件。 合约创建后无法访问日志数据,但可以从区块链外部高效访问数据。 由于部分日志数据存储在布隆过滤器中,我们可以通过一种高效且加密安全的方式搜索日志,因此那些没有下载整个区块链的网络节点(“轻客户端”)也可以找到这些日志。

创建

合约甚至可以通过特殊指令创建其他合约(而不是简单地调用零地址)。 创建合约的create调用和普通消息调用的唯一区别是会执行payload数据,执行结果会作为合约代码存储,调用者/创建者会获取新合约在栈上的地址.

自我毁灭

从区块链中删除合约代码的唯一方法是让合约对其合约地址执行自毁操作。 合约账户中剩余的以太币被发送到指定的目的地,其存储和代码从状态中移除。 (译者注,此处“从状态中移除”所说的“状态”指的是以太坊的“世界状态”,这是一个抽象的概念,可以理解为以太坊中所有账户的数据状态。快照是通过改进的前缀树数据结构。)

警告:即使合约代码没有显式调用selfdestruct,它仍然可以通过delegatecall 或callcode 执行自毁操作。

注意:旧合约的修剪可能会也可能不会由以太坊中的各种客户端程序实现。 此外,存档节点可以选择无限期保留合约存储和代码。

注意:目前,无法从状态中删除外部帐户。