# pragma pragma与许可证明 ```solidity // 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; } } // pragma版本:上述版本pragma solidity >=0.4.16 <0.9.0;指定该合约使用的solidity语言版本必须大于等于0.4.16,同时小于0.9.0。 // 注意:pragma solidity ^0.5.2指定该合约使用的solidity语言版本需要大于等于0.5.2,但是必须小于0.6.0。(这一切归因于^) ``` #区块链 ## 区块链概念 区块链是一个全球共享的数据库,每个区块链网络中的节点都可以对其进行读取。如果需要对数据库进行修改,需要发送交易来进行尝试。所谓尝试是说:这个交易必须被区块链网络中的其他节点所接收(共识)才有可能更改数据库的内容;如果交易的内容不合法(同时修改两个值),那么无法通过共识,因此无法修改(交易回滚)。 ## 区块 区块是交易的集合,如下图:世界状态在经过一个区块后,从t1态更新为t2态。 ![image](https://hackmd.io/_uploads/BJYpfw4sye.png) 权益证明(POS)的基本步骤: 1. 验证者向以太坊质押32个eth,用以遏制不良行为。 2. 每个时隙(12秒,slot,每个32个slot称为一个时段,epoch)会随机选择一个验证者作为区块提议者,将交易打包并执行以确定一个新的世界状态。而后,它将一系列信息包装到区块中,传送给其他验证者。 3. 其他验证者验证该区块的有效性,如果同意接收该区块,则将区块添加到自身数据库中。 4. 如果验证者检查到一个时隙中存在两个冲突区块(区块提议者发送两个区块到网络中,由于p2p网络的特性,不同的验证者会接收到不同的区块),它们可以用自己的分叉选择算法选择获得质押最多的那个区块。 需要注意的是:以太坊的权益证明会在每个时段的第一个区块(称为检查点)进行Justified和Finalized。如果检查点的区块Finalized,则该区块的所有祖先区块都会被Finalized。如果一个区块没有被Finalized,那么它还有可能回滚。这个过程如下: 一个区块被提议后,会广播给其他验证者,验证者验证通过后投prepare票,如果收集到2/3的prepare票(基于质押者数量),那么进入Justified状态。当区块Justified之后,验证者再次投commit票,如果收集到2/3的commit票,该区块将被Finalized。 ## 区块示例 区块示例:https://beaconscan.com/slot/10033886 ![image](https://hackmd.io/_uploads/BJV1XvEsyl.png) ![image](https://hackmd.io/_uploads/HJv1XwVsyl.png) ## 双花攻击 如果有一个账户的余额只有1ether,它同时向其他两个账户发送了1ether,也即将它的余额花费两次,这就是双花攻击。 在区块链领域中,上述行为需要同时发送两笔交易;而后这两笔交易在区块中会存在执行的先后顺序,如果交易1先执行,那么账户的余额已经为空,交易2就会被回滚。这里的关键点在于区块中已经为交易排序(这个排序经过了共识),因此如果要造成双花攻击,需要操纵共识机制。(2/3的验证者都是你的人!) ## EVM 概述 EVM是智能合约运行环境,它与外部网络环境完全隔离。 账户 EVM中账户分为EOA账户和合约账户,这些账户都是由私钥控制的。EOA账户的地址由公钥进行keccak256生成,而合约账户的地址是由下面的公式计算: `keccak256(rlp_encode(creator_account, creator_account_nonce))[12:]` 将合约创建者的地址和nonce进行rlp编码,得到的结果进行keccak256计算,最后截断尾部20字节。 一个账户拥有四个字段: 1. nonce:计数器,记录了当前账户发送交易的数量。 2. code_hash:字节码哈希,如果是EOA账户此字段为空。 3. balance:余额。 4. storage:slot与合约状态变量的映射,如果是EOA账户此字段为空。 关于nonce: 如果是EOA账户,每发送一笔交易其nonce++;如果是合约账户,每创建一个合约其nonce++。 nonce的作用之一是交易排序:如果你同时发送了两笔交易给矿工,矿工可根据你的nonce为交易排序。nonce的另一作用是防止重放攻击:一笔交易的nonce值代表了交易发起人发送交易的数量。如果一笔交易不包含nonce值,那么任何人都可以复制该交易重复执行。如果一笔交易包含nonce值,那么针对该交易发起者,其同一nonce值的交易只能被打包一次。 ## 交易 一笔交易本质上是一个账户发送给另一个账户的消息。 ``` EOA账户 -> 合约账户:调用合约中的函数。 合约账户 -> 合约账户:称为内部交易,属于合约之间的调用。 合约账户 -> EOA账户:可能是给EOA账户转账 EOA账户 -> EOA账户:转账。 ``` 如果交易的to地址为空,则说明此交易在创建合约。这种合约创建交易的calldata是合约的creationCode+构造函数参数。注意:在创建合约时(也即执行构造函数时),其字节码为空,因此在不应在构造函数中回调当前合约。 ## GAS 每笔交易在创建时都会被收取一定量的 gas,这笔费用必须由交易的发起人(tx.origin)支付。当以太坊虚拟机(EVM)执行交易时,gas 会根据特定规则逐渐耗尽。如果在任何时候 gas 被耗尽(即变为负数),就会触发 gas 耗尽异常,这会结束执行并将当前调用帧对状态所做的所有修改回滚。这种机制鼓励经济地使用 EVM 执行时间,并且也补偿 EVM 执行者(即矿工/质押者)的工作。由于每个区块都有最大的 gas 量,它也限制了验证一个区块所需的工作量。 gas 价格是由交易发起人设定的一个值,发起人必须预先支付 `gas_price * gas` 给 EVM 执行者。如果执行后还有剩余的 gas,将会退还给交易发起人。如果发生异常导致更改被回滚,已经用掉的 gas 是不会退还的。 由于 EVM 执行者可以选择是否包含一笔交易,因此交易发送者不能通过设定一个很低的 gas 价格来滥用系统。 ## Stack,Memory,Storage EVM具有三个可存储数据的空间:Stack、Memory和Storage。其中Stack、Memory是临时存储,而Storage是持久存储。 每个合约都维护自身的Storage,合约无法读取或写入其他合约的Storage(如果Storage修饰为public,可以访问)。 Memory和Stack会在每次消息调用重新创建(在创建前会保存其内容)。 ### Memory Memory就是一个**动态字节数组**,该数组限制读取宽度为32字节,而写入宽度可以是1字节或32字节。当读取或写入未触及的内存地址时,会进行内存扩展(每次扩展的单位时32字节,扩展完成后的内存长度为32字节整数倍)。在进行内存扩展时需要消耗gas。 一个内存扩展的例子 操作码:PUSH1 0xFF PUSH1 0X0 MSTORE PUSH1 0XFF PUSH1 0X22 MSTORE ![image](https://hackmd.io/_uploads/r1MJVP4iJg.png) ### Stack Stack是一个动态数组,该数组的每个元素长度为32字节,数组的最大长度为1024。EVM是一个堆栈机,一切计算都是在Stack中进行的。由于Stack仅限于访问顶端元素,因此对其他元素的读取依赖于Swap指令或Dup指令。 ## 指令集 EVM 的指令集保持最小化,以避免可能导致共识问题的不正确或不一致的实现。 ## Message Call=Call Meesage定义:在两个账户之间传输的数据(from、to、value、data等等),形式是合约的确定性操作或者经过加密安全签名的交易。 Message Call定义:将消息从一个账户传递到另一个账户,如果目标账户的字节码非空,则根据目标账户的账户以及消息的内容执行。且如果发送账户是一个合约账户,那么执行的返回值将立即返回。 由上述内容,可知Message Call就是一笔内部交易,也就是一般理解的Call。 在solidity合约内部可以指定一次Message Call对gas的使用量,如果本次Message Call由于gas耗尽失败,也只会消耗指定用量。Call调用栈的深度为1024,由于每次Message Call只能转发63/64的gas,因此实际上Call调用深度会小于1000。 ## Delegate Call Delegatecall是Message Call的变体,只是借用目标地址的代码在当前的上下文中执行(msg.sender和msg.value都不改变)。这意味着,合约可以在任意上下文动态加载其他合约的代码。 ## Logs Solidity的事件就是根据日志功能来实现的,日志可以将数据存储在特殊索引的数据结构中,每个区块都有一个该结构。合约无法直接访问日志中的数据。日志数据的一部分存储在布隆过滤器中,因此可以高效且加密的方式搜索这些数据,即使是轻客户端也可以找到这些日志内容。 ## Create 合约可以直接通过字节码来创建新的合约账户,包括Create和Create2字节码。 ## Selfdestruct 从区块链中删除代码的唯一办法是当前合约执行自毁操作,存储在该地址的剩余eth将被发送到指定目标账户,而后该合约账户的Storage和代码都被永久删除。 注意: 1. 在Cancun之后执行自毁操作只会将所有eth转移到指定目标地址,而不会销毁合约。即使合约执行了自毁操作,依然可以被调用。但是,如果在合约创建的同时执行自毁操作,还是可以达到销毁合约的目的。 2. 自毁已经被弃用了,但是未被删除。 3. 销毁合约和从硬盘中删除内容不同,其他的节点有可能依然保存了该合约的历史数据。 4. 自毁仅仅会将剩余eth发送给指定用户,而ERC20代币将永久丢失。 5. 如果指定目标账户不存在,会直接创建一个新的账户。 ## 预编译合约 预编译合约是 EVM 中用于提供更复杂库函数 ( 通常用于加密、散列等复杂操作 ) 的一种折衷方法,也可以理解为一种特殊的合约,这些函数不适合编写操作码。 它们适用于简单但经常调用的合约,或逻辑上固定但计算量很大的合约。 预编译合约是在使用节点客户端代码实现的,因为它们不需要 EVM,所以运行速度很快(直接编写在geth代码层面)。 与使用直接在 EVM 中运行的函数相比,它对开发人员来说成本也更低。