智能合约简介
简单的智能合同
让我们从一个基本示例开始,该示例设置变量的值并将其公开为其他合同访问。是否现在不了解所有内容都没关系,我们将稍后再详细解释。
存储示例
// 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; } }
第一行告诉您,源代码是根据GPL版本3.0许可的。第一行告诉您,源代码是根据GPL版本3.0授权的。在默认情况下发布源代码的环境中,机器可读的许可证说明符非常重要。
下一行指定源代码是为0.4.16或更高版本的版本编写的,但不包括0.9.0版。这是为了确保合同不会与新的(破坏性)编译器版本进行编译,这可能会导致不同的行为。它是关于如何处理源代码的通用编译器指令(例如一次)。
从某种意义上说,合同是位于以太坊区块链上特定地址的代码(其功能)和数据(其状态)的集合。 uint;该行声明了一个名为UINT的状态变量(256位签名的整数)。它可以被视为数据库中的一个插槽,可以通过调用管理数据库的函数来查询和更改。在此示例中,合同定义并获得可用于修改或检索变量值的函数。
要访问当前合同的成员(例如状态变量),您通常不需要添加此内容。前缀,只需直接以其名称访问它。与其他一些语言不同,忽略它不仅是一种风格问题,还导致了一种完全不同的访问成员的方式,但是稍后将进行详细讨论。
目前,此合同实际上没有做太多事情,除了(由于以太坊构建的基础架构)允许任何人存储一个单个数字,而世界上任何人都可以访问,而没有(可行的)方法可以防止您发布此数字。任何人都可以再次调用设置,并以不同的值覆盖您的电话号码,但是该数字仍存储在区块链的历史中。稍后,您将看到如何施加访问限制,以便只能更改此数字。
警告
使用文本时要小心,因为看起来相似(甚至相同)的字符可能具有不同的代码点,因此被编码为不同的字节数组。
评论
所有标识符(合同名称,函数名称和可变名称)都限于ASCII字符集。 UTF-8编码数据可以存储在字符串变量中。
亚货币示例
以下合同实施了最简单的加密货币形式。该合同仅允许其创建者创建新的硬币(可以使用不同的发行计划)。任何人都可以在不注册用户名和密码的情况下互相发送硬币,他们所需要的只是以太坊密钥对。
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; // 仅通过 IR 编译 contract Coin { // 关键字 "public" 使变量可从其他合约访问 address public minter; mapping(address => uint) public balances; // 事件允许客户端对你声明的特定合约更改做出反应 event Sent(address from, address to, uint amount); // 构造函数代码仅在合约被创建时运行 constructor() { minter = msg.sender; } // 向地址发送一定数量的新创建的币 // 只能由合约创建者调用 function mint(address receiver, uint amount) public { require(msg.sender == minter); balances[receiver] += amount; } // 错误允许你提供有关操作失败的原因的信息。 // 它们被返回给函数的调用者。 error InsufficientBalance(uint requested, uint available); // 从任何调用者向地址发送一定数量的现有币 function send(address receiver, uint amount) public { require(amount <= balances[msg.sender], InsufficientBalance(amount, balances[msg.sender])); balances[msg.sender] -= amount; balances[receiver] += amount; emit Sent(msg.sender, receiver, amount); } }
该合同介绍了一些新概念,让我们一一了解它们。
;该行声明类型的状态变量。类型是160位值,不允许任何算术操作。它适用于存储合同的地址,或属于关键对的公共密钥哈希。
关键字会自动生成一个函数,该函数允许从合同外部访问状态变量的当前值。没有此关键字,其他合同将无法访问变量。编译器生成的功能代码等于以下(并暂时查看):
function minter() external view returns (address) { return minter; }
您可以添加像上方一样的功能,但是将有一个函数和一个具有相同名称的状态变量。您无需这样做,编译器将为您处理。
下一行(=> uint);还创建一个公共状态变量,但它是一个更复杂的数据类型。键入地址。
地图可以被视为哈希,它们实际上是初始化的,因此从开始和图表到代表所有零的字节的每个可能的密钥都存在。但是,不可能获取映射所有键的列表,也无法获取所有值的列表。记录您添加到地图上的内容,或在不需要此信息的上下文中使用它。或者更好,请保留列表,或使用更合适的数据类型。
在映射的情况下,由关键字创建的更为复杂。看起来像这样:
function balances(address account) external view returns (uint) { return balances[account]; }
此功能可用于查询单个帐户的余额。
发送(从,到UINT)发送的事件;该行声明在函数发送的最后一行触发的线路。以太坊客户(例如Web应用程序)可以以低成本在区块链上收听这些事件。一旦触发它,侦听器就会接收到从和和和和跟踪交易的参数。要聆听此事件,您可以使用以下代码使用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)); } })
这是在合同创建期间执行的特殊功能,之后无法调用。在这种情况下,它将永久存储创建合同的人的地址。 MSG变量(与TX和Block一起)是包含允许访问区块链的属性。味精。始终是当前(外部)函数调用来自的地址。
组成合同的功能以及用户和合同可以调用的功能是薄荷和发送的功能。
薄荷功能将一定数量的新创建的硬币发送到另一个地址。功能调用定义条件,如果不满足,将撤销所有更改。在此示例中,(msg。==);确保只有合同的创建者才能称呼Mint。一般而言,创作者可以造成任何数量的令牌,但是在某些时候,这将导致一种称为“溢出”的现象。请注意,由于默认值,如果表达式[] +=;溢出,交易将被撤销,即[[] +在任何精确算术中都大于UINT的最大值时(2 ** 256-1)。对于函数发送[] +=;的语句也是如此。 。
允许为呼叫者提供有关条件或操作故障的更多信息。使用错误。该声明无条件地撤消和撤消所有变化,类似于。这两种方法都使您可以提供错误的名称和其他数据,这些数据将提供给呼叫者(最终到前端应用程序或阻止浏览器),以使调试或响应故障更容易。
任何人(已经拥有其中一些硬币的人)可以使用发送函数将硬币发送给他人。如果发件人没有足够的硬币发送,则IF条件将评估为true。因此,操作将失败,同时使用错误向发件人提供错误详细信息。
评论
如果您使用此合同将硬币发送到地址,那么当您在区块链浏览器上查看该地址时,您将看不到任何内容,因为您发送硬币的记录和更改余额仅存储在此特定硬币合同的数据存储中。通过使用事件,您可以创建一个“区块链浏览器”,以跟踪新硬币的交易和余额,但是您必须检查硬币合同地址,而不是硬币所有者的地址。
区块链基础知识
对于程序员来说,区块链作为一个概念并不难。原因是大多数复杂性(采矿, - 弯曲,点对点等)只是为了提供一组特定的功能和对平台的承诺。一旦您接受这些功能是既定的事实,就不必担心基础技术,或者您必须知道亚马逊的AWS在内部使用它如何使用它?
贸易
区块链是一个全球共享的交易数据库。这意味着每个人都可以通过参与网络来阅读数据库中的条目。如果您想更改数据库中的内容,则必须创建一个所谓的交易,其他所有人都必须接受。术语事务意味着您要进行的更改(假设您要同时更改两个值)要么完全执行或完全应用。同样,当您的交易应用于数据库时,没有其他交易可以更改它。
例如,想象一张表,以电子货币列出所有帐户的余额。如果您要求从一个帐户转移到另一个帐户,则数据库的交易性质确保如果从一个帐户中减去金额,则将始终将其添加到另一个帐户中。如果由于某种原因无法将金额添加到目标帐户中,则源帐户也不会修改。
此外,交易始终由发件人(创建者)加密和签名。这使得可以轻松保护对数据库特定修改的访问。对于电子货币,简单的支票可确保只有那些持有帐户密钥的人才能从中转移一些补偿,例如以太。
块
一个主要的障碍是克服比特币术语中所谓的“双重支付攻击”:如果网络中有两项交易都想清除一个帐户,会发生什么?只有一项交易可以有效,通常是第一个要接受的交易。问题在于,“第一个”不是点对点网络中的客观术语。
对此的抽象答案是您不必关心。网络将选择全球接受的交易订单,以解决冲突。这些交易将包装到所谓的“块”中,然后将它们执行并在所有参与节点之间分发。如果两项交易相互冲突,则最终是第二笔交易将被拒绝,并且不会成为块的一部分。
这些块形成线性时间序列,这是“区块链”一词的来源。尽管这些间隔可能会在将来发生变化,但以固定的间隔添加块。为了获取最新信息,建议监视网络,例如上述网络。
作为“顺序选择机制”的一部分,可能会不时将块被撤销,但仅在链的“尖端”上被撤销。在特定块上添加的块越多,撤销该块的可能性就越小。因此,您的交易可能会被撤销甚至从区块链中删除,但是您等待的时间越长,它的可能性就越小。
评论
不保证交易将包含在下一个块或任何特定的将来块中,因为这取决于矿工决定包括交易的决定,而不是交易提交者。
如果您想安排未来的呼叫合同,则可以使用智能合同自动化工具或服务。
以太坊虚拟机
概述
以太坊虚拟机(EVM)是以太坊智能合约的运行环境。它不仅是沙盒环境,而且实际上是完全隔离的,这意味着在EVM内部运行的代码无法访问网络,文件系统或其他进程。智能合约甚至可以使用其他智能合约。
帐户
以太坊中有两种类型的帐户共享相同的地址空间:外部帐户由公私密钥对(即人类)控制,合同帐户由存储在帐户中的代码控制。
外部帐户的地址由公共密钥确定,合同的地址是在合同创建时确定的(它源自创建者地址和从该地址发送的交易数量,即所谓的“ nonce”)。
无论帐户是否存储代码,这两种类型在EVM中均平等处理。
每个帐户都有一个持久的键值存储,该存储将256位单词映射到256位单词,称为存储。
此外,每个帐户在以太中都有一个余额(确切地说,“ WEI”,1醚为10 ** 18 WEI),可以通过发送包含以太的交易来修改。
贸易
交易是从一个帐户发送到另一个帐户的消息(也许是相同的或空的,请参见下文)。它可以包含二进制数据(称为“有效载荷”)和以太。
如果目标帐户包含代码,则执行代码,并作为输入数据提供有效载荷。
如果未设置目标帐户(交易没有接收器或将接收器设置为null),则交易将创建新合同。如前所述,合同的地址不是零地址,而是从发送者获得的地址和发送的交易数(“ nonce”)。创建交易的此类合同的有效载荷被视为EVM字节码并执行。此执行的输出数据将永久存储为合同的代码。这意味着要创建合同,您不会发送合同的实际代码,而是实际发送执行时返回该代码的代码。
评论
在合同创建期间,其代码保持空。因此,在构造函数执行完成之前,您不应回电。
气体
创建时,每笔交易都会有一定数量的气体,并由交易的发起人(TX。)支付。当EVM执行交易时,气体会根据特定规则逐渐耗尽。如果在任何时候耗尽了气体(即变为负数),则会触发过气的异常,结束执行并在当前呼叫框架中撤消对状态的所有修改。
这种机制激励了EVM执行时间的经济使用,并补偿了EVM执行者(即矿工/利益相关者)的工作。由于每个块的气体量最大,因此它还限制了验证块所需的工作量。
汽油价格是交易的发起人设定的价值,发起人必须提前向EVM执行人支付 *汽油。如果执行后仍然剩下一些气体,则将返回交易发起人。除了取消变化外,使用的气体不会退回。
由于EVM执行人可以选择是否包括交易,因此交易发件人不能通过设置低汽油价格滥用系统。
存储,临时存储,内存和堆栈
以太坊虚拟机有不同的区域可以存储数据,其中最著名的是存储,临时存储,内存和堆栈。
每个帐户都有一个称为存储的数据区域,该数据区域在函数调用和交易之间持续存在。存储是一个钥匙值商店,将256位单词映射到256位单词。不可能从合同中列举存储,阅读相对较贵,初始化和修改存储空间更昂贵。由于这一成本,您应该最大程度地减少存储在持续存储中的内容与合同需要运行的内容。商店得出合同外的计算,缓存和聚合数据。合同不能读取或写入任何存储空间以外的存储空间。
与存储相似,还有另一个称为临时存储的数据区域,主要区别在于它在每个交易结束时重置。仅在交易的第一个调用中,仅在函数调用之间维护存储在此数据位置中的值。交易完成后,将重置临时存储,并且在其中存储的值在后续交易中无法呼叫。然而,阅读和写作临时存储的成本明显低于存储的成本。
第三个数据区域称为内存,合同为每个消息调用获得了一个新清除的实例。内存是线性的,可以在字节级别上解决,但是阅读限制为256位,而写入可以是8位或256位宽。访问以前未触摸的记忆单词(无论是读取还是写)时,记忆会通过单词(256位)扩展。扩大时,必须支付汽油费。记忆成本随着增长而增加(其增长是正方形的)。
EVM不是寄存器机器,而是堆栈机,因此所有计算均在称为堆栈的数据区域执行。它的最大尺寸为1024个元素,包含256位单词。对堆栈的访问仅限于顶部,并以以下方式执行:可以将前16个元素之一复制到堆栈顶部,或者堆栈的顶部元素与下面的16个元素之一交换。所有其他操作都从堆栈中占据顶部两个(或一个或多个,取决于操作),然后将结果推到堆栈上。当然,可以将堆栈元素移至存储或内存,以更深入地访问堆栈,但是如果不先删除堆栈的顶部元素,就不可能直接访问堆栈中的任何元素。
调用数据,返回数据和代码
还有其他数据领域不像前面讨论的那样明显。但是,它们在执行智能合同交易期间通常使用。
呼叫数据区域是作为智能合约事务的一部分发送到交易的数据。例如,创建合同时,呼叫数据将是新合同的构造函数代码。外部函数的参数始终存储在ABI编码中的呼叫数据中,然后解码为声明中指定的位置。如果被声明为AS,则编译器将在开始时热切地将功能解码为内存,并将其标记为访问时会懒惰地进行。值类型和指针直接解码为堆栈。返回数据是智能合约在呼叫后返回值的方式。一般而言,外部功能使用关键字将值ABI编码到返回数据区域中。
代码是为智能合约存储EVM指令的区域。代码是EVM在智能合约执行期间读取,解释和执行的字节。代码中存储的指令数据是合同帐户状态字段的一部分。不变和恒定变量存储在代码区域中。对不变变量的所有引用都被分配给它们的值所取代。常数以类似的方式处理,表达式在智能合同代码中被列为所引用的位置。
指令集
EVM的指示集被尽量减少,以避免可能导致共识问题的不正确或不一致的实现。所有指令均在基本数据类型256位单词或内存切片(或其他字节数组)上运行。可以使用常见的算术,位,逻辑和比较操作。有条件的和无条件的跳跃都是可能的。此外,合同可以访问当前块的相关属性,例如其编号和时间戳。
有关完整列表,请参见内联汇编文档的一部分。
消息通话
合同可以通过消息通话来调用其他合同或将以太币发送到非合同帐户。消息调用类似于交易,因为它们具有源,目的地,数据负载,以太,气体和返回数据。实际上,每个交易都由一个顶级消息调用组成,这又创建了更多消息调用。
合同可以确定其剩余的气体应随着内部消息调用发送,以及它希望保留多少。如果内部呼叫具有耗尽的气体异常(或任何其他异常),则将通过放置在堆栈上的错误值来发出信号。在这种情况下,只需消耗带有通话的汽油。在这种情况下,拨打合同将导致默认情况下的手动例外,因此呼叫堆栈的例外“气泡”。
如前所述,称为合同(可以与呼叫者相同)将收到一个新清除的内存实例,并可以访问调用负载 - 这将在一个单独的区域中提供。执行完成后,它可以返回数据,该数据将存储在呼叫者的预先分配内存位置中。所有此类呼叫均已完全同步。
呼叫的深度限制为1024,这意味着对于更复杂的操作,应使用循环代替递归呼叫。此外,在消息通话中只能转发63/64的气体,实际上,这导致深度限制仅低于1000。
委托呼叫和图书馆
有一个特殊的消息调用变体,与消息调用相同,唯一的区别是,目标地址的代码是在调用合同(即地址)和味精的值中执行的。和味觉不变。
这意味着合同可以在运行时从不同的地址中动态加载代码。存储,当前地址和余额仍然指向调用合同,仅从调用地址获得代码。
这使得可以在:可重复使用的库代码中实现“库”函数,例如可以将合同存储,例如实现复杂的数据结构。
日志
数据可以存储在特殊的索引数据结构中,该数据结构将其映射到块级别。此功能称为记录的功能用于实现。合同创建后无法访问日志数据,但是可以从区块链外部有效地访问它。由于一部分日志数据是在Bloom中存储的,因此可以以高效,加密且安全的方式搜索此数据,因此无法下载整个区块链的网络节点(所谓的“ Light客户端”)仍然可以找到这些日志。
创造
合同甚至可以使用特殊的创建其他合同(即它们不仅会调用零地址,例如交易)。这些创建呼叫和普通消息调用之间的唯一区别是执行有效载荷数据,并将结果存储为代码,呼叫者/创建者在堆栈上接收新合同的地址。
禁用和自我毁灭
从区块链中删除代码的唯一方法是该地址上的合同执行操作时。存储在该地址的剩余的以太硬币将发送到指定的目的地,并将存储和代码从州删除。从理论上讲,删除合同听起来可能是一个好主意,但是这可能是危险的,因为如果某人将以太发送到删除的合同中,这些以太将永远丢失。
警告
从EVM> =开始,只有帐户中的所有以太将被发送到指定的收件人而不会销毁合同。但是,当指称创建称为合同的同一交易时,硬叉之前的行为(即EVM)