智能合约
一个存储的例子
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16
第一行告诉您源代码是在 GPL 3.0 版下获得许可的。机器可读的许可证说明符在默认发布源代码的设置中非常重要。
第二行指定使用solidity的0.4.16版本或更高版本,但要小于(不包括0.9.0)。这确保了合约不会使用新的编译版本,因为有些版本会有不同的表现。Pragmas是编译器关于如何处理源代码的通用指令。
Solidity意义上的合约是驻留在以太坊区块链上特定地址的代码(其功能)和数据(其状态)的集合。该行 uint storedData
;声明一个名为storedData
的状态变量,类型为uint
(256位无符号整数)。您可以将其视为数据库中的单个槽,您可以通过调用管理数据库的代码的函数来查询和更改它。在此示例中,合约定义了可用于修改或检索变量值的函数set
和get
。
要访问当前合约的成员(如状态变量),您通常不会添加this.
前缀,您只需通过其名称直接访问它。与某些其他语言不同,省略它不仅仅是样式问题,它会导致以完全不同的方式访问成员,但稍后会详细介绍。
一个关于货币的例子
以下合约实现了最简单的加密货币形式。 该合约只允许其创建者创建新币(可能有不同的发行方案)。 任何人都可以互相发送硬币,而无需使用用户名和密码进行注册,您只需要一个以太坊密钥对。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// "public"关键字使得其他合约可以访问
address public minter;
mapping(address => uint) public balances;
// "event"允许客户端对合约出发的事件做出反应处理
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允许你提供操作失败的信息,并返回给方法的调用者
error InsufficientBalance(uint requested, uint available);
// 发送一定数量已存在的货币从方法调用者到一个地址
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;
声明一个地址类型的状态变量。 地址类型是一个 160 位的值,不允许任何算术运算。 它适用于存储合约地址,或属于外部账户的密钥对的公共部分的哈希值。
public
自动生成一个允许外部合约访问状态变量当前值的函数。没有public
关键字,其他合约无法访问该变量。编译器生成的函数代码等价于:
function minter() external view returns (address) { return minter; }
mapping(address => uint) public balances;
也会创建一个公共状态变量,但是它是一个更复杂的数据类型。
映射可以看作是哈希表,它实际上是经过初始化的,因此每个可能的键从一开始就存在,并映射到一个字节表示全为零的值。
在映射的情况下,由 public
关键字创建的 getter
函数更为复杂。 它看起来像下面这样:
function balances(address account) external view returns (uint) {
return balances[account];
}
您可以使用此功能查询单个账户的余额。
event Sent(address from, address to, uint amount);
声明了一个事件,在send
函数最后被触发。以太坊客户端(例如web应用)可以监听在区块链上出发的事件并且没有太多花费。一旦他被触发,监听者会接收from
、to
和amount
参数,方便追踪交易。
为了监听事件,您可以使用下面这个js代码,这段代码使用了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));
}
})
构造函数是一个特殊的函数,在合约创建过程中执行,之后无法调用。 在这种情况下,它会永久存储创建合约的人的地址。 msg
变量(还有 tx
和 block
)是一个特殊的全局变量,它包含允许访问区块链的属性。 msg.sender
始终是当前(外部)函数调用的来源地址。
mint
函数将一定数量的新创建的硬币发送到另一个地址。 require
函数调用定义了在不满足时会滚所有更改的条件。 在这个例子中,require(msg.sender == minter);
确保只有合约的创建者才能调用 mint
。 一般来说,创建者可以铸造任意数量的代币,但在某些时候,这会导致一种称为“溢出”的现象。 请注意,由于默认的 Checked 算法,如果表达式 balances[receiver] += amount;
溢出,交易将回滚,即当 balances[receiver] + amount
在任意精度算法中大于 uint
的最大值 (2**256 - 1)
。 这也适用于在send
函数中的语句 balances[receiver] += amount;
。
error
允许您向调用者提供错误信息。error
与 revert
语句一起使用。 revert
语句无条件中止并会滚所有更改,类似于 require
函数,但它还允许您提供异常的名称和提供给调用者的附加数据(并最终提供给前端应用程序或区块浏览器),这样就可以更容易地调试或响应故障。
任何人(已经拥有其中一些货币的人)都可以使用send
功能将货币发送给其他任何人。 如果发件人没有足够的货币发送,则 if
条件的计算结果为真,revert
会触发操作失败,同时使用 InsufficientBalance 错误向发送方提供错误详细信息。
区块链基础
交易
区块链是一个全球共享的交易数据库。 这意味着每个人都可以通过参与网络来读取数据库中的条目。 如果你想改变数据库中的某些东西,你必须创建一个必须被所有其他人接受的所谓的交易。 交易这个词意味着你想要做的改变(假设你想同时改变两个值)要么根本没有完成,要么完全应用。 此外,当您的事务应用于数据库时,没有其他事务可以更改它。
例如,假设有一个表格,其中列出了电子货币中所有账户的余额。 如果请求从一个账户转移到另一个账户,数据库的交易性质确保如果从一个账户中减去金额,它总是被添加到另一个账户。 如果由于某种原因,无法将金额添加到目标账户,源账户也不会被修改。
此外,交易总是由发送者(创建者)加密签名。 这使得保护对数据库特定修改的访问变得简单明了。 在电子货币的例子中,一张简单的支票确保只有持有账户钥匙的人才能从中转账。
区块
要克服的一个主要障碍是(用比特币术语来说)所谓的“双花攻击”:如果网络中存在两笔交易都想清空一个账户,会发生什么? 只有一个交易可以是有效的,通常是第一个被接受的交易。 问题是“第一”在点对点网络中并不是一个客观的术语。
对此的抽象答案是您不必关心。 将为您选择一个全局接受的交易顺序,解决冲突。交易将被捆绑到所谓的“块”中,然后它们将在所有参与节点中执行和分发。 如果两笔交易相互矛盾,最后排在第二位的交易将被拒绝,并且不会成为区块的一部分。
这些块在时间上形成线性序列,这就是“区块链”一词的来源。 块会定期添加到链中,尽管这些间隔将来可能会发生变化。 要获得最新信息,建议监控网络,例如,在 Etherscan 上。
作为“顺序选择机制”(称为“挖掘”)的一部分,块可能会不时被回滚,但仅限于链的“尖端”。 在特定区块之上添加的区块越多,该区块被回滚的可能性就越小。 因此,您的交易可能会被回滚甚至从区块链中删除,但您等待的时间越长,这种可能性就越小。
🥸 交易不能保证包含在下一个区块或任何特定的未来区块中,因为这不是由交易的提交者决定的,而是由矿工决定交易被包含在哪个区块中。
EVM
以太坊虚拟机或 EVM 是以太坊中智能合约的运行时环境。 它不仅是沙盒化的,而且实际上是完全隔离的,这意味着在 EVM 内运行的代码无法访问网络、文件系统或其他进程。 智能合约甚至对其他智能合约的访问也受到限制。
账户
以太坊中有两种共享同一地址空间的账户:由公私钥对(即人类)控制的外部账户和由与账户存储在一起的代码控制的合约账户。
外部账户的地址由公钥确定,而合约的地址是在合约创建时确定的(它来自创建者地址和从该地址发送的交易数量,即所谓的“ 随机数”)。
无论帐户是否存储代码,EVM 都对这两种类型一视同仁。
每个账户都有一个持久的键值存储,将 256 位字符映射到 256 位字符,称为存储。
此外,每个账户都有以太币余额(准确地说,以“Wei”为单位,1 以太币是 10**18 wei),可以通过发送包含以太币的交易来修改。
交易
交易是从一个帐户发送到另一个帐户的消息(可能相同或为空,见下文)。 它可以包括二进制数据(称为“payload”)和以太币。
如果目标帐户包含代码,则执行该代码并将payload作为输入数据提供。
如果未设置目标账户(交易没有收件人或收件人设置为空),交易将创建一个新合约。 如前所述,该合约的地址不是零地址,而是从发送方及其发送的交易数量(“随机数”)派生的地址。 这种合约创建交易的payload被视为 EVM 字节码并被执行。 此执行的输出数据作为合约代码永久存储。 这意味着为了创建合约,您不发送合约的实际代码,而是发送执行时返回该代码的代码。
🥸 在创建合约时,其代码仍然是空的。 因此,在其构造函数完成执行之前,您不应该回调正在构建的合约。
Gas
创建时,每笔交易都会收取一定数量的燃料费,必须由交易发起人 (tx.origin) 支付。 在 EVM 执行交易的同时,gas 会按照特定的规则逐渐耗尽。 如果 gas 在任何时候用完(即它会是负数),就会触发 out-of-gas 异常,这会结束执行并回滚对当前调用帧中的状态所做的所有修改。
这种机制鼓励合理使用EVM 执行时间,并补偿 EVM 执行者(即矿工/质押者)的工作。 由于每个区块都有最大的气体量,因此它也限制了验证区块所需的工作量。
gas price 是由交易的发起者设定的值,他必须预先向 EVM 执行者支付 gas_price * gas
。 如果执行后还剩下一些gas,则将其退还给交易发起人。 如果出现回滚更改的异常,则不会退还已用完的气体。
由于 EVM 执行者可以选择是否包含交易,交易发送者不能通过设置低 gas 价格来滥用系统。
存储、内存和堆栈
以太坊虚拟机具有三个可以存储数据的区域:存储、内存和堆栈。
每个账户都有一个称为存储(storage)的数据区域,它在函数调用和交易之间是持久的。 storage是一种键值存储,将 256 位字符映射到 256 位字符。 无法从合约中枚举存储,读取成本相对较高,初始化和修改存储的成本更高。 由于这种成本,您应该将持久存储中存储的内容最小化为合约运行所需的内容。 在合约外部存储派生计算、缓存和聚合等数据。 合约既不能读取也不能写入除它自己之外的任何storage。
第二个数据区域称为内存(memory),合约为每个消息调用获取一个实例。 内存是线性的,可以在字节级别寻址,但读取宽度限制为 256 位,而写入宽度可以是 8 位或 256 位。 当访问(读取或写入)以前未触及的内存字(即字内的任何偏移量)时,内存会扩展一个字(256 位)。 扩建时,必须支付gas费用。 内存越大,成本就越高(它以二次方式扩展)。
EVM 不是寄存器机而是堆栈机,所以所有的计算都在称为堆栈的数据区域上进行。 它的最大大小为 1024 个元素,包含 256 位的字。 通过以下方式,对堆栈的访问仅限于顶端:可以将最顶端的 16 个元素之一复制到堆栈的顶部,或者将最顶端的元素与其下方的 16 个元素之一交换。 所有其他操作从堆栈中取出最顶层的两个(或一个或多个,取决于操作)元素并将结果压入堆栈。 当然,可以将堆栈元素移动到存储或内存中,以便更深入地访问堆栈,但如果不先移除堆栈顶部,就不可能访问堆栈中更深层的任意元素。
指令集
EVM 的指令集保持最小化,以避免可能导致共识问题的不正确或不一致的实现。 所有指令都可以对基本数据类型、256 位字或内存片(或其他字节数组)进行操作。 常用的算术、位、逻辑和比较操作都存在。 此外,合约可以访问当前区块的相关属性,例如其编号和时间戳。
消息调用
合约可以调用其他合约或通过消息调用的方式向非合约账户发送以太币。 消息调用类似于交易,因为它们有源、目标、数据有效负载、以太币、gas 和返回数据。 事实上,每个交易都包含一个顶级消息调用,而该消息调用又可以创建进一步的消息调用。
合约可以决定应通过内部消息调用发送多少剩余气体,以及要保留多少。 如果在内部调用中发生 out-of-gas 异常(或任何其他异常),这将通过放入堆栈的错误值发出信号。 在这种情况下,只有随调用一起发送的气体用完了。 在 Solidity 中,调用合约在这种情况下默认会导致手动异常,因此异常会“冒泡”调用堆栈。
如前所述,被调用的合约(可以与调用者相同)将收到一个可清除的内存实例,并可以访问调用payload——这将在一个称为calldata的单独区域中提供。 执行完成后,它可以返回数据,这些数据将存储在调用者预先分配的调用者内存中的某个位置。 所有此类调用都是完全同步的。
调用深度限制为 1024,这意味着对于更复杂的操作,循环应该优先于递归调用。 此外,在消息调用中只能转发 63/64 的气体,这导致实际深度限制略小于 1000。
委托调用和库
https://learnblockchain.cn/article/4310
合约可以在运行时从不同的地址动态加载代码。 存储、当前地址和余额仍然参考调用合约,只是代码取自被调用地址。
这使得在 Solidity 中实现“库”功能成为可能:可重用的库代码可应用于合约的存储,例如 为了实现复杂的数据结构。
日志
可以将数据存储在一个专门索引的数据结构中,该结构一直映射到块级别。 Solidity 使用这个称为日志的功能来实现事件。 合约创建后无法访问日志数据,但可以从区块链外部有效地访问它们。 由于部分日志数据存储在布隆过滤器中,因此可以以高效且加密安全的方式搜索此数据,因此不下载整个区块链的网络对等点(所谓的“轻客户端”)仍然可以 找到这些日志。
创建合约
合约甚至可以使用特殊的操作码创建其他合约(即它们不像交易那样简单地调用零地址)。 这些创建调用和普通消息调用之间的唯一区别是有效载荷数据被执行,结果存储为代码,调用者/创建者在堆栈上接收新合约的地址。
停用和自毁
从区块链中删除代码的唯一方法是当该地址的合约执行selfdestruct
操作时。 存储在该地址的剩余 Ether 被发送到指定的目标,然后存储和代码从状态中删除。 理论上移除合约听起来是个好主意,但它有潜在的危险,就好像有人向移除的合约发送以太币,以太币就永远丢失了。
如果你想停用你的合约,你应该通过更改一些导致所有功能恢复的内部状态来禁用它们。 这使得无法使用合约,但是它会立即返回 Ether。
预编译合约
https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/design/virtual_machine/precompiled.html
本文由mdnice多平台发布
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net