返回

以太坊综述

以太坊基础

以太坊综述

主要内容:以太坊整体介绍

以太坊特点

  • 以太坊是“世界计算机”,这代表它是一个开源的、全球分布的计算基础设施
  • 执行称为智能合约(smart contract)的程序
  • 使用区块链来同步和存储系统状态以及名为以太币(ether)的加密货币,以计量和约束执行资源成本
  • 本质是一个基于交易的状态机(transaction-based state machine)
  • 以太坊平台使开发人员能够构建具有内置经济功能的强大去中心化应用程序(DApp);在持续自我正常运行的同时,它还减少或消除了审查,第三方界面和交易对手风险

以太坊的组成部分

P2P网络
  • 以太坊在以太坊主网络上运行,该网络可在TCP端口30303上寻址,并运行一个名为 ÐΞVp2p的协议
交易(Transaction)
  • 以太坊交易是网络消息,其中包括发送者(sender),接收者(receiver),值(value) 和数据的有效载荷(payload)
以太坊虚拟机(EVM)
  • 以太坊状态转换由以太坊虚拟机(EVM)处理,这是一个执行字节码(机器语言指令)的 基于堆栈的虚拟机
数据库(Blockchain)
  • 以太坊的区块链作为数据库(通常是 Google 的 LevelDB)本地存储在每个节点上,包含 序列化后的交易和系统状态
客户端
  • 以太坊有几种可互操作的客户端软件实现,其中最突出的是 Go-Ethereum(Geth)和 Parity

以太坊中的重要概念

账户(Account)
  • 包含地址,余额和随机数,以及可选的存储和代码的对象
  • 普通账户(EOA),存储和代码均为空
  • 合约账户(Contract),包含存储和代码
地址(Address)
  • 一般来说,这代表一个EOA或合约,它可以在区块链上接收或发送交易
  • 更具体地说,它是ECDSA 公钥的 keccak 散列的最右边的160位
交易(Transaction)
  • 可以发送以太币和信息
  • 向合约发送的交易可以调用合约代码,并以信息数据为函数参数
  • 向空用户发送信息,可以自动生成以信息为代码块的合约账户
gas
  • 以太坊用于执行智能合约的虚拟燃料
  • 以太坊虚拟机使用核算机制来衡量gas的消耗量并限制计算资源的消耗

以太坊的货币

  • 以太坊的货币单位称为以太(ether),也可以表示为ETH或符号Ξ
  • 以太币的发行规则:
    • 挖矿前(Pre-mine,Genesis)
      • 2014年7月/8月间,为众筹大约发行了7200万以太币。这些币有的时候被称之为“矿前”。众筹阶段之后,以太币每年的产量基本稳定,被限制不超过7200万的25%
    • 挖矿产出(Mining)
      • 区块奖励(block reward)
      • 叔块奖励(uncle reward)
      • 叔块引用奖励(uncle referencing reward)
  • 以太币产量未来的变化
    • 以太坊出块机制从工作量证明(PoW)转换为股权证明(PoS)后,以太币的发行会有什么变化尚未有定论。股权证明机制将使用一个称为Casper的协议。在Casper协议下,以太币的发行率将大大低于目前幽灵(GHOST)协议下的发行率

以太坊的挖矿产出

区块奖励(Block rewards)
  • 每产生一个新区块就会有一笔固定的奖励给矿工,初始是5个以太币,现在是3个
叔块奖励(Uncle rewards)
  • 有些区块被挖得稍晚一些,因此不能作为主区块链的组成部分。比特币称这类区块为“孤块”,并且完全舍弃它们。但是,以太币称它们为“叔块”(uncles),并且在之后的区块中,可以引用它们。如果叔块在之后的区块链中作为叔块被引用,每个叔块会为挖矿者产出区块奖励的7/8。这被称之为叔块奖励
叔块引用奖励(Uncle referencing rewards)
  • 矿工每引用一个叔块,可以得到区块奖励的1/32作为奖励(最多引用两个叔块)

  • 这样的一套基于POW的奖励机制,被称为以太坊的“幽灵协议”

以太坊区块收入

普通区块收入
  • 固定奖励(挖矿奖励),每个普通区块都有
  • 区块内包含的所有程序的 gas 花费的总和
  • 如果普通区块引用了叔块,每引用一个叔块可以得到固定奖励的 1/32
叔块收入
  • 叔块收入只有一项,就是叔块奖励,计算公式为:
  • 叔块奖励 = ( 叔块高度 + 8 – 引用叔块的区块高度 ) * 普通区块奖励 / 8

“幽灵”(GHOST)协议

  • 以太坊出块时间:设计为12秒,实际14~15秒左右
  • 快速确认会带来区块的高作废率,由此链的安全性也会降低
  • “幽灵”协议:Greedy Heaviest Observed SubTree,“GHOST”
    • 计算工作量证明时,不仅包括当前区块的祖区块,父区块,还要包括祖先块的作废的后代区块(“叔块”),将他们进行综合考虑。
    • 目前的协议要求下探到第七层(最早的简版设计是五层),也就是说,废区块只能以叔区块的身份被其父母的第二代至第七代后辈区块引用,而不能是更远关系的后辈区块。
    • 以太坊付给以“叔区块”身份为新块确认作出贡献的废区块7/8的奖励,把它们纳入计算的“侄子区块”将获得区块奖励的1/32,不过,交易费用不会奖励给叔区块。

以太坊和图灵完备

  • 1936年,英国数学家艾伦·图灵(Alan Turing)创建了一个计算机的数学模型,它由一个控制器、一个读写头和一根无限长的工作带组成。纸带起着存储的作用,被分成一个个的小方格(可以看成磁带);读写头能够读取纸带上的信息,以及将运算结果写进纸带;控制器则负责根据程序对搜集到的信息进行处理。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动纸带
  • 如果一个系统可以模拟任何图灵机,它就被定义为“图灵完备”(Turing Complete)的。 这种系统称为通用图灵机(UTM)
  • 以太坊能够在称为以太坊虚拟机的状态机中执行存储程序,同时向内存读取和写入数据, 使其成为图灵完备系统,因此成为通用图灵机。考虑到有限存储器的限制,以太坊可以计 算任何可由任何图灵机计算的算法
  • 简单来说,以太坊中支持循环语句,理论上可以运行“无限循环”的程序

去中心化应用

  • 基于以太坊可以创建智能合约(Smart Contract)来构建去中心化应用(Decentralized Application,简称为 DApp)
  • 以太坊的构想是成为 DApps 编程开发的平台
  • DApp至少由以下组成:
    • 区块链上的智能合约
    • Web前端用户界面

以太坊应用

  • 基于以太坊创建新的加密货币(CryptoCurrency,这种能力是 2017 年各种 ICO 泛滥的技术动因)
  • 基于以太坊创建域名注册系统、博彩系统
  • 基于以太坊开发去中心化的游戏,比如 2017 年底红极一时的以太猫(CryptoKitties,最高单只猫售价高达80W美元)

代币(Token)

  • 代币(token)也称作通证,本意为“令牌”,代表有所有权的资产、货币、权限等在区块链上的抽象
  • 可替代性通证(fungible token):指的是基于区块链技术发行的,互相可以替代的,可以接近无限拆分的token
  • 非同质通证(non-fungible token): 指的是基于区块链技术发行的,唯一的,不可替代的,大多数情况下不可拆分的token,如加密猫(CryptoKitties)

名词解释

  • EIP: Ethereum Improvement Proposals,以太坊改进建议
  • ERC:Ethereum Request for Comments的缩写,以太坊征求意见。一些EIP被标记为ERC,表示试图定义以太坊使用的特定标准的提议
  • EOA:External Owned Account,外部账户。由以太坊网络的人类用户创建的账户
  • Ethash:以太坊1.0 的工作量证明算法
  • HD钱包:使用分层确定性(HD protocol)密钥创建和转账协议(BIP32)的钱包
  • Keccak256:以太坊中使用的密码哈希函数。Keccak256 被标准化为SHA-3
  • Nonce:在密码学中,术语nonce用于指代只能使用一次的值。以太坊使用两种类型的随机数,账户随机数和POW随机数

初识以太网

主要内容:钱包、测试网、简单交易

以太币单位

  • 以太坊的货币单位称为以太,也称为ETH或符号Ξ
  • ether被细分为更小的单位,直到可能的最小单位,称为wei;1 ether = 10^18 wei
  • 以太的值总是在以太坊内部表示为以wei表示的无符号整数值
  • 以太的各种单位都有一个使用国际单位制(SI)的科学名称,和一个口语名称

以太坊钱包

以太坊钱包是我们进入以太坊系统的门户。它包含了私钥,可以代表我们创建和广播交易

  • MetaMask:一个浏览器扩展钱包,可在浏览器中运行
  • Jaxx:一款多平台、多币种的钱包,可在各种操作系统上运行,包括 Android,iOS,Windows,Mac和Linux
  • MyEtherWallet(MEW):一个基于web的钱包,可以在任何浏览器中运行
  • Emerald Wallet:旨在与 ETC 配合使用,但与其他基于以太坊的区块链兼容

私钥、公钥和地址

  • 私钥(Private Key):以太坊私钥事实上只是一个256位的随机数,用于发送以太的交易 中创建签名来证明自己对资金的所有权
  • 公钥(Public Key):公钥是由私钥通过椭圆曲线加密secp256k1算法单向生成的512位 (64字节)数
  • 地址(Address):地址是由公钥的 Keccak-256 单向哈希,取最后20个字节(160位) 派生出来的标识符

安全须知

  • keystore文件就是加密存储的私钥。所以当系统提示你选择密码时:将其设置为强密码,备份并不要共享。如果你没有密码管理器,请将其写下来并将其存放在带锁的抽屉或保险箱中。要访问账户,你必须同时有keystore文件和密码
  • 助记词可以导出私钥,所以可以认为助记词就是私钥。请使用笔和纸进行物理备份。不要把这个任务留给“以后”,你会忘记
  • 切勿以简单形式存储私钥,尤其是以电子方式存储
  • 不要将私钥资料存储在电子文档、数码照片、屏幕截图、在线驱动器、加密PDF等中。使用密码管理器或笔和纸
  • 在转移任何大额金额之前,首先要做一个小的测试交易(例如,小于1美元)。收到测试交易后,再尝试从该钱包发送

助记词

  • 助记词是明文私钥的另一种表现形式,最早由BIP-39提出,目的是帮助用户记忆复杂的私钥(256位)
  • 技术上该提议可以在任意区块链中实现,比如使用完全相同的助记词在比特币和区块链上生成的地址可以是不同的,用户只需要记住满足一定规则的词组(就是上面说的助记词),钱包软件就可以基于该词组创建一些列的账户,并且保障不论是在什么硬件、什么时间创建出来的账户、公钥、私钥都完全相同,这样既解决了账号识记的问题,也把账户恢复的门槛降低了很多
  • 支持 BIP39 提议的钱包也可以归类为 HD 钱包(Hierarchical Deterministic Wallet),Metamask 当属此类

在 Remix 上构建简单的水龙头合约

  • Ether 用于支付运行智能合约的费用,智能合约是在称为以太坊虚拟机(EVM)的模拟计算机上运行的计算机程序

  • EVM 是一个全局单例,意味着它就像是一个全局的单实例计算机一样运行,无处不在。以太坊网络上的每个节点都运行 EVM 的本地副本以验证合约执行,而以太坊区块链在处理交易和智能合约时记录此世界计算机的变化状态

  • 以太坊有许多不同的高级语言,所有这些语言都可用于编写合约并生成 EVM 字节码。到目前为止,只有一种高级语言是智能合约编程的主要语言:Solidity

  • Solidity 由 Gavin Wood 创建,并已成为以太坊及其他地区使用最广泛的语言。我们将使用 Solidity 编写我们的第一份合约

编写水龙头合约
  • 水龙头是一件相对简单的事情:它会向任何要求的地址发出以太,并且可以定期重新填充
  • 我们可以将水龙头实现为由人(或 Web 服务器)控制的钱包,不过现在我们的目标是学习智能合约,所以我们将编写实现水龙头的 Solidity 合同:

Faucet.sol:实现水龙头的 Solidity 合同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.19;
// Our first contract is a faucet!
contract Faucet {
    // Give out ether to anyone who asks
    function withdraw(uint withdraw_amount) public {
        // Limit withdrawal amount
        require(withdraw_amount <= 100000000000000000);
        // Send the amount to the address that requested it
        msg.sender.transfer(withdraw_amount);
    }
    // Accept any incoming amount
    function () public payable {}
}
  • 这是一个非常简单的合约,尽可能简单。它也是一个有缺陷的合同,有一些不良做法和安全漏洞
  • contract Faucet {
    • 该行声明了一个合约对象,类似于其他面向对象语言中的类声明
  • function withdraw(uint withdraw_amount) public {
    • 函数名withdraw,它接受一个名为withdraw_amount的无符号整数(uint参数。它被声明为公共函数,这意味着它可以被其他合约调用
  • require(withdraw_amount <= 100000000000000000);
    • 该行设定提款限额
    • 使用内置Solidity函数require来测试一个前提条件,即withdraw_amount小于或等于100000000000000000 wei,这是ether的 基本单位,相当于0.1 ether
    • 如果使用大于该数量的withdraw_amount调用withdraw函数,则此处的require函数将导致合约执行停止并因异常而失败
  • msg.sender.transfer(withdraw_amount);
    • 该行是实际提现
    • msg对象:所有合约都可以访问的输入之一,它表示触发此合约执行的交易
    • sender属性:交易的发件人地址
    • transfer函数:是一个内置函数,它将以太合约传递到调用它的地址。向后读,这意味着将以太转移到触发此合约执行的msg的发送者
    • transfer函数将金额作为其唯一参数。我们将withdraw_amount值作为参数传递给上面几行声明的withdraw函数
  • function () public payable {}
    • 此函数是所谓的“回退”或默认函数,如果触发合约的交易未命名合约中的任何已声明函数或任何函数或未包含数据,则调用此函数
编译水龙头合约
  • 现在我们有了第一个示例合约,我们需要使用 Solidity 编译器将 Solidity 代码转换为 EVM 字节码,以便它可以由 EVM 执行
  • Solidity 编译器作为独立的可执行文件,作为不同框架的一部分,也捆绑在集成开发环境(IDE)中。为了简单起见,我们将使用一种比较流行的 IDE,称为 Remix
  • Remix 是以太坊社区开发并开源的、一款非常好用的在线 Solidity 集成开发环境,我 们可以方便的在其中编写、部署、测试智能合约,Remix 提供了强大的自动完成,语法高亮,实时编译检查错误等功能
在区块链上创建合同
  • 我们写了合约并把它编译成字节码。现在,我们需要在以太坊区块链上“注册” 合约。我们将使用测试网来测试我们的合约
  • 在区块链上注册合约涉及创建一个特殊交易,其目的地是一个“零地址”,也就是地址为:0x0000000000000000000000000000000000000000。零地址是一个特殊地址,告诉以太坊区块链我们想要注册合约。不过我们不需要手动输入这么多个 0,Remix IDE 将为我们处理所有这些并将交易发送到 MetaMask
与合约交互
  • 以太坊合约是控制资金的程序,它在称为 EVM 的虚拟机内运行。它们由特殊交易创建,该交易提交其字节码以记录在区块链上。一旦他们在区块链上创建,他们就有了一个以太坊地址,就像钱包一样。只要有人将某个交易发送到合约地址,就会导致合约在 EVM 中运行,并将该合约作为其输入
  • 发送到合约地址的交易可能包含 ether 或数据或两者。如果它们含有 ether,则将其“存入”合约余额。如果它们包含数据,则数据可以在合约中指定命名函数并调用它,将参数传递给函数
资助合约
  • 目前,合约在其历史记录中只有一个交易:合约创建交易
  • 合约也还没有以太(零余额)。那是因为我们没有在创建交易中向合约发送任何以太
  • 我们可以给合约发一些以太,打开 MetaMask,给合约的地址发送以太,就像发送给其他任何以太坊地址一样
  • 当交易发送到合同地址时,没有数据指定要调用的函数,它会调用默认函数
提现合约
  • 接下来,让我们从水龙头中提取一些资金。要提现,我们必须构造一个调用withdraw函数的交易,并将 withdraw_amount参数传递给它。为了使事情变得简单,Remix 将为我们构建该交易,MetaMask 将提供它以供我们批准
  • 该交易导致合约在 EVM 内部运行,当 EVM 运行水龙头合约的提现功能时,首先它调用require函数并验证我们的金额小于或等于允许的最大提现 0.1 以太;然后它调用transfer函数向我们发送以太,运行转账功能会产生一个内部交易,从合约的余额中将 0.1 以太币存入我们的钱包地址;该笔交易会显示在 etherscan 中“内部交易”选项卡中
小结
  • 我们在 Solidity 写了一个水龙头合约,然后使用 Remix IDE 将合约编译为 EVM 字节码;我们使用 Remix 进行交易,并在区块链测试网上记录了水龙头合约。 一旦记录下来,水龙头合约就有一个以太坊地址,我们给它发了一些 ether。最后,我们构建了一个交易来调用withdraw函数并成功请求 0.1 ether。合约检查了我们的请求,并通过内部交易向我们发送了 0.1 以太

以太坊客户端

主要内容:客户端、Geth的安装和使用、搭建私链

什么是以太坊客户端

  • 以太坊客户端是一个软件应用程序,它实现以太坊规范并通过p2p网络与其他以太坊客户端进行通信。如果不同的以太坊客户端符合参考规范和标准化通信协议,则可以进行相互操作
  • 以太坊是一个开源项目,由“黄皮书”正式规范定义。除了各种以太坊改进提案之外,此正式规范还定义了以太坊客户端的标准行为
  • 因为以太坊有明确的正式规范,以太网客户端有了许多独立开发的软件实现,它们之间又可以彼此交互

基于以太坊规范的网络

  • 存在各种基于以太坊规范的网络,这些网络基本符合以太坊“黄皮书”中定义的形式规范,但它们之间可能相互也可能不相互操作
  • 这些基于以太坊的网络中有:以太坊,以太坊经典,Ella,Expanse,Ubiq,Musicoin等等
  • 虽然大多数在协议级别兼容,但这些网络通常具有特殊要求,以太坊客户端软件的维护人员、需要进行微小更改、以支持每个网络的功能或属性

以太坊的多种客户端

  • go-ethereum ( Go )
    • 官方推荐,开发使用最多
    • Geth是由以太坊基金会积极开发的 Go 语言实现,因此被认为是以太坊客户端的“官方”实现
    • 通常,每个基于以太坊的区块链都有自己的Geth实现
  • parity ( Rust )
    • 最轻便客户端,在历次以太坊网络攻击中表现卓越
  • cpp-ethereum (C++)
  • pyethapp (python)
  • ethereumjs ( javascript )
  • EthereumJ / Harmony ( Java )

JSON-RPC

  • 以太坊客户端提供了API和一组远程调用(RPC)命令,这些命令被编码为 JSON。这被称为 JSON-RPC API。本质上,JSON-RPC API 就是一个接口,允许我们编写的程序使用以太坊客户端作为网关,访问以太坊网络和链上数据
  • 通常,RPC 接口作为一个 HTTP 服务,端口设定为 8545。出于安全原因,默认情况下,它仅限于接受来自 localhost 的连接
  • 要访问JSON-RPC API,我们可以使用编程语言编写的专用库,例如JavaScript的 web3.js
  • 或者也可以手动构建HTTP请求并发送/接收JSON编码的请求

以太坊全节点

  • 全节点是整个主链的一个副本,存储并维护链上的所有数据,并随时验证新区块的合法性
  • 区块链的健康和扩展弹性,取决于具有许多独立操作和地理上分散的全节点。每个全节点都可以帮助其他新节点获取区块数据,并提供所有交易和合约的独立验证
  • 运行全节点将耗费巨大的成本,包括硬件资源和带宽
  • 以太坊开发不需要在实时网络(主网)上运行的全节点。我们可以使用测试网络的节点来代替,也可以用本地私链,或者使用服务商提供的基于云的以太坊客户端;这些几乎都可以执行所有操作

远程客户端和轻节点

  • 远程客户端
    • 不存储区块链的本地副本或验证块和交易。这些客户端一般只提供钱包的功能,可以创建和广播交易。远程客户端可用于连接到现有网络,MetaMask 就是一个这样的客户端
  • 轻节点
    • 不保存链上的区块历史数据,只保存区块链当前的状态。轻节点可以对块和交易进行验证

全节点的优缺点

  • 优点
    • 为以太坊网络的灵活性和抗审查性提供有力支持
    • 权威地验证所有交易
    • 可以直接与公共区块链上的任何合约交互
    • 可以离线查询区块链状态(帐户,合约等)
    • 可以直接把自己的合约部署到公共区块链中
  • 缺点
    • 需要巨大的硬件和带宽资源,而且会不断增长
    • 第一次下载往往需要几天才能完全同步
    • 必须及时维护、升级并保持在线状态以同步区块。

公共测试网络节点的优缺点

  • 优点
    • 一个 testnet 节点需要同步和存储更少的数据,大约10GB,具体取决于不同的网络
    • 一个 testnet 节点一般可以在几个小时内完全同步
    • 部署合约或进行交易只需要发送测试以太,可以从“水龙头”免费获得
    • 测试网络是公共区块链,有许多其他用户和合约运行(区别于私链)
  • 缺点
    • 测试网络上使用测试以太,它没有价值。因此,无法测试交易对手的安全性,因为没有任何利害关系
    • 测试网络上的测试无法涵盖所有的真实主网特性。例如,交易费用虽然是发送交易所必需的,但由于gas免费,因此 testnet 上往往不会考虑。而且一般来说,测试网络不会像主网那样经常拥堵

本地私链的优缺点

  • 优点
    • 磁盘上几乎没有数据,也不同步别的数据,是一个完全“干净”的环境
    • 无需获取测试以太,你可以任意分配以太,也可以随时自己挖矿获得
    • 没有其他用户,也没有其他合约,没有任何外部干扰
  • 缺点
    • 没有其他用户意味与公链的行为不同。发送的交易并不存在空间或交易顺序的竞争
    • 除自己之外没有矿工意味着挖矿更容易预测,因此无法测试公链上发生的某些情况
    • 没有其他合约,意味着你必须部署要测试的所有内容

用Geth搭建以太坊私链

一种是直接用源码安装,直接克隆 git 仓库,以获取源代码的副本

1
git clone https://github.com/ethereum/go-ethereum.git

另一种是到官网直接下载对应系统的安装程序

查看geth version,确保在真正运行之前安装正常

启动节点同步

一般不需要节点同步,因为耗时耗力

安装好了 Geth,现在我们可以尝试运行一下它。执行下面的命令,geth 就会开始同步区块,并存储在当前目录下。这里的--syncmode fast参数表示我们会以“快速”模式同步区块。在这种模式下,我们只会下载每个区块头和区块体,但不会执行验证所有的交易,直到所有区块同步完毕再去获取一个系统当前的状态。这样就节省了很多交易验证的时间

1
 geth datadir . --syncmode fast

如果我们想同步测试网络的区块,可以用下面的命令:

1
 geth --testnet --datadir . --syncmode fas

--testnet这个参数会告诉 geth 启动并连接到最新的测试网络,也就是Ropsten,测试网络的区块和交易数量会明显少于主网,所以会更快一点。但即使是用快速模式同步测试网络,也会需要几个小时的时间

搭建自己的私链

因为公共网络的区块数量太多,同步耗时太长,我们为了方便快速了解 Geth,可以试着用它来搭一个只属于自己的私链

首先,我们需要创建网络的“创世”(genesis)状态,这写在一个小小的 JSON 文件里(例如,我们将其命名为 genesis.json):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "config": {
        "chainId": 15
    },
    "difficulty": "2000",
    "gasLimit": "2100000",
    "alloc": {
        "7df9a875a174b3bc565e6424a0050ebc1b2d1d82": { "balance": "300000" },
        "f41c74c9ae680c1aa78f42e5647a62f353b7bdde": { "balance": "400000" }
    }
}

要创建一条以它作为创世块的区块链,我们可以使用下面的命令:

1
geth --datadir path/to/custom/data/folder init genesis.json

在当前目录下运行 geth,就会启动这条私链,注意要将networked设置为与创世块配置里的chainId一致\

1
geth --datadir path/to/custom/data/folder --networkid 15

现在,节点正常启动,恭喜!我们已经成功启动了一条自己的私链

Geth 控制台命令

Geth Console 是一个交互式的 JavaScript 执行环境,里面内置了一些用来操作以太坊的 JavaScript 对象,我们可以直接调用这些对象来获取区块链上的相关信息。这些对象主要包括:

  • eth:主要包含对区块链进行访问和交互相关的方法;
  • net:主要包含查看 p2p 网络状态的方法;
  • admin:主要包含与管理节点相关的方法;
  • miner:主要包含挖矿相关的一些方法;
  • personal:包含账户管理的方法;
  • txpool:包含查看交易内存池的方法;
  • web3:包含以上所有对象,还包含一些通用方法

常用命令有:

  • personal.newAccount():创建账户;
  • personal.unlockAccount():解锁账户;
  • eth.accounts:列出系统中的账户;
  • eth.getBalance():查看账户余额,返回值的单位是 Wei;
  • eth.blockNumber:列出当前区块高度;
  • eth.getTransaction():获取交易信息;
  • eth.getBlock():获取区块信息;
  • miner.start():开始挖矿;
  • miner.stop():停止挖矿;
  • web3.fromWei():Wei 换算成以太币;
  • web3.toWei():以太币换算成 Wei;
  • txpool.status:交易池中的状态;

深入理解以太坊

以太坊账户和合约

主要内容:账户详解、合约特性

从UTXO谈起

  • 比特币在基于UTXO的结构中存储有关用户余额的数据:系统的整个状态就是一组UTXO的集合,每个UTXO都有一个所有者和一个面值 (就像不同的硬币),而交易会花费若干个输入的UTXO,并根据规则创建若干个新的UTXO:
  • 每个引用的输入必须有效且尚未花费;对于一个交易,必须包含有与每个输入的所有者匹配的签名;总输入必须大于等于总输出值
  • 所以,系统中用户的余额(balance)是用户具有私钥的 UTXO 的总值

以太坊的做法

  • 以太坊的“状态”,就是系统中所有帐户的列表
  • 每个账户都包括了一个余额(balance),和以太坊特殊定义的数据(代码和内部存储)
  • 如果发送帐户有足够的余额来支付,则交易有效;在这种情况下发送帐户先扣款,而收款帐户将记入这笔收入
  • 如果接收帐户有相关代码,则代码会自动运行,并且它的内部存储也可能被更改,或者代码还可能向其他帐户发送额外的消息,这就会导致进一步的借贷资金关系

优缺点比较

比特币 UTXO 模式优点:

  • 更高程度的隐私:如果用户为他们收到的每笔交易使用新地址,那么通常很难将帐户相互链接。这很大程度上适用于货币,但不太适用于任意dapps,因为dapps通常涉及跟踪和用户绑定的复杂状态,可能不存在像货币那样简单的用户状态划分方案
  • 潜在的可扩展性:UTXO在理论上更符合可扩展性要求。因为我们只需要依赖拥有 UTXO 的那些人去维护基于Merkle树的所有权证明就够了,即使包括所有者在内的每个人都决定忘记该数据,那么也只有所有者受到对应UTXO的损失,不影响接下来的交易。而在帐户模式中,如果每个人都丢失了与帐户相对应的Merkle树的部分,那将会使得和该帐户有关的消息完全无法处理,包括发币给它

以太坊账户模式优点:

  • 可以节省大量空间:不将 UTXOs 分开存储,而是合为一个账户;每个交易只需要一个输入、一个签名并产生一个输出
  • 更好的可替代性:货币本质上都是同质化、可替代的;UTXO的设计使得货币从来源分成了“可花费”和“不可花费”两类,这在实际应用中很难有对应的模型
  • 更加简单:更容易编码和理解,特别是设计复杂脚本的时候。UTXO 在脚本逻辑复杂时更令人费解
  • 便于维护持久轻节点:只要沿着特定方向扫描状态树,轻节点可以很容易地随时访问账户相关的所有数据。而UTXO的每个交易都会使得状态引用发生改变,这对轻节点来说长时间运行Dapp会有很大压力

以太坊账户类型

  • 外部账户 (Externally owned account, EOA )
  • 合约账户 (Contract accounts)
EOA

外部账户(用户账户/普通账户)

  • 有对应的以太币余额
  • 可发送交易(转币或触发合约代码)
  • 由用户私钥控制
  • 没有关联代码
合约账户
  • 有对应的以太币余额
  • 有关联代码
  • 由代码控制
  • 可通过交易或来自其它合约的调用消息来触发代码执行
  • 执行代码时可以操作自己的存储空间,也可以调用其它合约

以太坊交易、gas和EVM

主要内容:交易详解、EVM简介

以太坊交易(Transaction)

签名的数据包,由EOA发送到另一个账户

  • 消息的接收方地址
  • 发送方签名
  • 金额(VALUE)
  • 数据(DATA,可选)
  • START GAS
  • GAS PRICE

消息(Message)

合约可以向其它合约发送“消息”

消息是不会被序列化的虚拟对象,只存在于以太坊执行环境 (EVM)中

可以看作函数调用

  • 消息发送方
  • 消息接收方
  • 金额(VALUE)
  • 数据(DATA,可选)
  • START GA

合约(Contract)

  • 可以读/写自己的内部存储(32字节key-value的数据库)
  • 可向其他合约发送消息,依次触发执行
  • 一旦合约运行结束,并且由它发送的消息触发的所有子执行(sub-execution)结束,EVM就会中止运行,直到下次交易被唤醒

合约应用一

  • 维护一个数据存储(账本),存放对其他合约或外部世界有用的内容
  • 最典型的例子是模拟货币的合约(代币)

合约应用二

  • 通过合约实现一种具有更复杂的访问策略的普通账户(EOA),这被称为“转发合同”:只有在满足某些条件时才会将传入的消息重新发送到某个所需的目的地址;例如,一个人可以拥有一份转发合约,该合约会等待直到给定三个私钥中的两个确认之后,再重新发送特定消息
  • 钱包合约是这类应用中很好的例子

合约应用三

  • 管理多个用户之间的持续合同或关系
  • 这方面的例子包括金融合同,以及某些特定的托管合同或某种保险

以太坊交易详解

交易的本质

  • 交易是由外部拥有的账户发起的签名消息,由以太坊网络传输,并被序列化后记录在以太坊区块链上
  • 交易是唯一可以触发状态更改或导致合约在EVM中执行的事物
  • 以太坊是一个全局单例状态机,交易是唯一可以改变其状态的东西
  • 合约不是自己运行的,以太坊也不会“在后台”运行。以太坊上的一切变化都始于交易

交易数据结构

交易是包含以下数据的序列化二进制消息:

  • nonce:由发起人EOA发出的序列号,用于防止交易消息重播
  • gas price:交易发起人愿意支付的gas单价(wei)
  • start gas:交易发起人愿意支付的最大gas量
  • to:目的以太坊地址
  • value:要发送到目的地的以太数量
  • data:可变长度二进制数据负载(payload)
  • v,r,s:发起人EOA的ECDSA签名的三个组成部分
  • 交易消息的结构使用递归长度前缀(RLP)编码方案进行序列化,该方案专为在以太坊中准确和字节完美的数据序列化而创建

交易中的nonce

  • 黄皮书定义: 一个标量值,等于从这个地址发送的交易数,或者对于关联code的帐户来说,是这个帐户创建合约的数量
  • nonce不会明确存储为区块链中帐户状态的一部分。相反,它是通过计算发送地址的已确认交易的数量来动态计算的
  • nonce值还用于防止错误计算账户余额。nonce强制来自任何地址的交易按顺序处理,没有间隔,无论节点接收它们的顺序如何
  • 使用nonce确保所有节点计算相同的余额和正确的序列交易,等同于用于防止比特币“双重支付”(“重放攻击”)的机制。但是,由于以太坊跟踪账户余额并且不单独跟踪 UTXO ,因此只有在错误地计算账户余额时才会发生“双重支付”。nonce机制可以防止这种情况发生

并发和nonce

  • 以太坊是一个允许操作(节点,客户端,DApps)并发的系统,但强制执行单例状态。例如,出块的时候只有一个系统状态
  • 假如我们有多个独立的钱包应用或客户端,比如 MetaMask和 Geth,它们可以使用相同的地址生成交易。如果我们希望它们都够同时发送交易,该怎么设置交易的nonce呢?
  • 用一台服务器为各个应用分配nonce,先来先服务——可能出现单点故障,并且失败的交易会将后续交易阻塞
  • 生成交易后不分配nonce,也不签名,而是把它放入一个队列等待。另起一个节点跟踪nonce并签名交易。同样会有单点故障的可能,而且跟踪nonce和签名的节点是无法实现真正并发的

交易中的gas

  • 当由于交易或消息触发 EVM 运行时,每个指令都会在网络的每个节点上执行。这具有成本:对于每个执行的操作,都存在固定的成本,我们把这个成本用一定量的 gas 表示
  • gas 是交易发起人需要为 EVM 上的每项操作支付的成本名称。发起交易时,我们需要从执行代码的矿工那里用以太币购买 gas
  • gas 与消耗的系统资源对应,这是具有自然成本的。因此在设计上 gas 和 ether 有意地解耦,消耗的 gas 数量代表了对资源的占用,而对应的交易费用则还跟 gas 对以太的单价有关。这两者是由自由市场调节的:gas 的价格实际上是由矿工决定的,他们可以拒绝处理 gas 价格低于最低限额的交易。我们不需要专门购买 gas ,只需将以太币添加到帐户即可,客户端在发送交易时会自动用以太币购买汽油。而以太币本身的价格通常由于市场力量而波动

gas的计算

  • 发起交易时的 gas limit 并不是要支付的 gas 数量,而只是给定了一个消耗 gas 的上限,相当于“押金”
  • 实际支付的 gas 数量是执行过程中消耗的 gas (gasUsed),gas limit 中剩余的部分会返回给发送人
  • 最终支付的 gas 费用是 gasUsed 对应的以太币费用,单价由设定的 gasPrice 而定
  • 最终支付费用 totalCost = gasPrice * gasUsed • totalCost 会作为交易手续费(Tx fee)支付给矿工

交易的接收者(to)

  • 交易接收者在to字段中指定,是一个20字节的以太坊地址。地址可以是EOA或合约地址
  • 以太坊没有进一步的验证,任何20字节的值都被认为是有效的。如果20字节值对应于没有相应私钥的地址,或不存在的合约,则该交易仍然有效。以太坊无法知道地址是否是从公钥正确派生的
  • 如果将交易发送到无效地址,将销毁发送的以太,使其永远无法访问
  • 验证接收人地址是否有效的工作,应该在用户界面一层完成

交易的 value 和 data

  • 交易的主要“有效负载”包含在两个字段中:value 和 data。交易可以同时有 value 和 data,仅有 value,仅有 data,或者既没有 value 也没有 data。所有四种组合都有效
  • 仅有 value 的交易就是一笔以太的付款
  • 仅有 data 的交易一般是合约调用
  • 进行合约调用的同时,我们除了传输 data, 还可以发送以太,从而交易中同时包含 data 和 value
  • 没有 value 也没有 data 的交易,只是在浪费 gas,但它是有效的

向 EOA 或合约传递 data

  • 当交易包含数据有效负载时,它很可能是发送到合约地址的,但它同样可以发送给 EOA
  • 如果发送 data 给 EOA,数据负载(data payload) 的解释取决于钱包
  • 如果发送数据负载给合约地址,EVM 会解释为函数调用,从 payload 里解码出函数名称和参数,调用该函数并传入参数
  • 发送给合约的数据有效负载是32字节的十六进制序列化编码:
    • 函数选择器:函数原型的 Keccak256 哈希的前4个字节。这允许 EVM 明确地识别将要调用的函数
    • 函数参数:根据 EVM 定义的各种基本类型的规则进行编码

特殊交易:创建(部署)合约

  • 有一种特殊的交易,具有数据负载且没有 value,那就是一个创建新合约的交易
  • 合约创建交易被发送到特殊目的地地址,即零地址0x0。该地址既不代表 EOA 也不代表合约。它永远不会花费以太或发起交易,它仅用作目的地,具有特殊含义“创建合约”
  • 虽然零地址仅用于合同注册,但它有时会收到来自各种地址的付款。 这种情况要么是偶然误操作,导致失去以太;要么是故意销毁以太
  • 合约注册交易不应包含以太值,只包含合约的已编译字节码的数据有效负载。此交易的唯一效果是注册合约

以太坊虚拟机(EVM)简介

以太坊虚拟机(EVM)

  • 以太坊虚拟机 EVM 是智能合约的运行环境
  • 作为区块验证协议的一部分,参与网络的每个节点都会运行 EVM。他们会检查正在验证的块中列出的交易,并运行由 EVM中的交易触发的代码
  • EVM不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行的代码是无法访问网络、文件系统和其他进程的, 甚至智能合约之间的访问也是受限的
  • 合约以字节码的格式(EVM bytecode)存在于区块链上
  • 合约通常以高级语言(solidity)编写,通过EVM编译器编译为字节码,最终通过客户端上载部署到区块链网络中

EVM和账户

  • 以太坊中有两类账户: 外部账户合约账户,它们共用 EVM中同一个地址空间
  • 无论帐户是否存储代码,这两类账户对 EVM 来说处理方式是完全一样的
  • 每个账户在EVM中都有一个键值对形式的持久化存储。其中 key 和 value 的长度都是256位,称之为存储空间 (storage)

EVM和交易

  • 交易可以看作是从一个帐户发送到另一个帐户的消息,它可以包含二进制数据(payload)和以太币
  • 如果目标账户含有代码,此代码会在EVM中执行,并以 payload 作为入参,这就是合约的调用
  • 如果目标账户是零账户(账户地址为 0 ),此交易就将创建一个新合约 ,这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行,执行的输出作为合约代码永久存储

EVM和gas

  • 合约被交易触发调用时,指令会在全网的每个节点上执行:这需要消耗算力成本;每一个指令的执行都有特定的消耗,gas 就用来量化表示这个成本消耗
  • 一经创建,每笔交易都按照一定数量的 gas 预付一笔费用,目的是限制执行交易所需要的工作量和为交易支付手续费
  • EVM 执行交易时,gas 将按特定规则逐渐耗尽
  • gas price 是交易发送者设置的一个值,作为发送者预付手续费的单价。如果交易执行后还有剩余, gas 会原路返还
  • 无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚

EVM数据存储

  • Storage
    • 每个账户都有一块持久化的存储空间,称为 storage,这是一个将256位字映射到256位字的 key-value 存储区,可以理解为合约的数据库
    • 永久储存在区块链中,由于会永久保存合约状态变量,所以读写的 gas 开销也最大
  • Memory(内存)
    • 每一次消息调用,合约会临时获取一块干净的内存空间
    • 生命周期仅为整个方法执行期间,函数调用后回收,因为仅保存临时变量,故读写 gas 开销较小
  • Stack(栈)
    • EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为栈(stack)的区域执行
    • 存放部分局部值类型变量,几乎免费使用的内存,但有数量限制

EVM指令集

  • 所有的指令都是针对"256位的字(word)“这个基本的数据类型来进行操作
  • 具备常用的算术、位、逻辑和比较操作,也可以做到有条件和无条件跳转
  • 合约可以访问当前区块的相关属性,比如它的块高度和时间戳

消息调用( Message Calls )

  • 合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户
  • 合约可以决定在其内部的消息调用中,对于剩余的 gas ,应发送和保留多少
  • 如果在内部消息调用时发生了 out-of-gas 异常(或其他任何异常),这将由一个被压入栈顶的错误值所指明;此时只有与该内部消息调用一起发送的 gas 会被消耗掉

委托调用(Delegatecall)

  • 一种特殊类型的消息调用
  • 目标地址的代码将在发起调用的合约的上下文中执行,并且msg.sendermsg.value不变
  • 可以由此实现“库”(library):可复用的代码库可以放在一个合约的存储上,通过委托调用引入相应代码

合约的创建和自毁

  • 通过一个特殊的消息调用create calls,合约可以创建其他合约(不是简单的调用零地址)
  • 合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作selfdestruct;合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除

以太坊编程及应用

Solidity基础

主要内容:Solidy语法、简单合约

Solidity是什么

  • Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在以太坊虚拟机(EVM)上运行
  • Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性
  • 内含的类型除了常见编程语言中的标准类型,还包括 address 等以太坊独有的类型,Solidity 源码文件通常以 .sol 作为扩展名
  • 目前尝试 Solidity 编程的最好的方式是使用 Remix。Remix 是一个基于 Web 浏览器的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约

Solidity语言特性

Solidity的语法接近于JavaScript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同:

  • 以太坊底层基于帐户,而不是 UTXO,所以增加了一个特殊的 address 的数据类型用于定位用户和合约账户
  • 语言内嵌框架支持支付。提供了 payable 等关键字,可以在语言层面直接支持支付
  • 使用区块链进行数据存储。数据的每一个状态都可以永久存储,所以在使用时需要确定变量使用内存,还是区块链存储
  • 运行环境是在去中心化的网络上,所以需要强调合约或函数执行的调用的方式
  • 不同的异常机制。一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致

Solidity源码和智能合约

Solidity 源代码要成为可以运行在以太坊上的智能合约需要经历如下的步骤:

  1. 用 Solidity 编写的智能合约源代码需要先使用编译器编译为字节码(Bytecode),编译过程中会同时产生智能合约的二进制接口规范 (Application Binary Interface,简称为ABI)
  2. 通过交易(Transaction)的方式将字节码部署到以太坊网络,每次成功部署都会产生一个新的智能合约账户
  3. 使用 Javascript 编写的 DApp 通常通过 web3.js + ABI去调用智能合约中的函数来实现数据的读取和修改

Solidity编译器

  • Remix
    • Remix 是一个基于 Web 浏览器的 Solidity IDE;可在线使用而无需安装任何东西
  • solcjs
    • solc 是 Solidity 源码库的构建目标之一,它是 Solidity 的命令行编译器
    • 使用 npm 可以便捷地安装 Solidity 编译器 solcjs

智能合约概述

Solidity中合约

  • 一组代码(合约的函数 )和数据(合约的状态 ),它们位于以太坊区块链的一个特定地址上
  • 代码行 uint storedData; 声明一个类型为 uint (256位无符号整数)的状态变量,叫做 storedData
  • 函数 set 和 get 可以用来变更或取出变量的值

合约结构

  • 状态变量(State Variables)
    • 作为合约状态的一部分,值会永久保存在存储空间内
  • 函数(Functions)
    • 合约中可执行的代码块
  • 函数修饰器(Function Modifiers)
    • 用在函数声明中,用来补充修饰函数的语义
  • 事件(Events)
    • 非常方便的 EVM 日志工具接口

一个简单的智能合约

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pragma solidity >0.4.22 <0.6.0; //一个例子:子货币
contract Coin { 
    address public minter; 
    mapping (address => uint) public balances; 
    event Sent(address from, address to, uint amount); 
    constructor() public { minter = msg.sender; } 
    function mint(address receiver, uint amount) public { 
        require(msg.sender == minter);
        balances[receiver] += amount; 
    } 
    function send(address receiver, uint amount) public { 
        require(amount <= balances[msg.sender]);
        balances[msg.sender] -= amount; 
        balances[receiver] += amount; 
        emit Sent(msg.sender, receiver, amount);
	}
}

address public minter;

  • 这一行声明了一个可以被公开访问的 address 类型的状态变量
  • 关键字 public 自动生成一个函数,允许你在这个合约之外访问这个状态变量的当前值

mapping(address => uint) public balances;

  • 这一行也是创建一个公共状态变量,但它是一个更复杂的数据类型, 该类型将 address 映射为无符号整数
  • mappings 可以看作是一个哈希表,它会执行虚拟初始化,把所有可能存在的键都映射到一个字节表示为全零的值

event Sent(address from, address to, uint amount);

  • 声明了一个“事件”(event),它会在send函数的最后一行触发
  • 用户可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知
  • 所有的事件都包含了fromtoamount三个参数,可方便追踪事务

emit Sent(msg.sender, receiver, amount);

  • 触发Sent事件,并将参数传入

Solidity深入理解

Solidity源文件布局

pragma (版本杂注)

  • 源文件可以被版本杂注pragma所注解,表明要求的编译器版本
  • 例如:pragma solidity ^0.4.0;
  • 源文件将既不允许低于0.4.0版本的编译器编译, 也不允许高于(包含)0.5.0版本的编译器编译(第二个条件因使用 ^ 被添加)

import (导入其它源文件)

  • Solidity 所支持的导入语句import,语法同 JavaScript(从 ES6 起)非常类似
  • 例如:import "filename";
    • filename中导入所有的全局符号到当前全局作用域中
  • 例如:import * as symbolName from "filename";
    • 创建一个新的全局符号symbolName,其成员均来自 filename中全局符号
  • 例如:import {symbol1 as alias, symbol2} from "filename";
    • 创建新的全局符号aliassymbol2,分别从 filename引用symbol1symbol2
  • 例如:import "filename" as symbolName;
    • 这条语句等同于import * as symbolName from "filename";

Solidity值类型

  • 布尔(bool):可能的取值为字符常量值 true 或 false
  • 整型(int/uint):分别表示有符号和无符号的不同位数的整型变量; 支持关键字 uint8 到 uint256(无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增
  • 定长浮点型(fixed / ufixed): 表示各种大小的有符号和无符号的定长浮点型;在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数
  • 地址(address):存储一个 20 字节的值(以太坊地址大小)
  • 定长字节数组:关键字有 bytes1, bytes2, bytes3, …, bytes32
  • 枚举(enum):一种用户可以定义类型的方法,与C语言类似,默认从0开始递增,一般用来模拟合约的状态
    • 枚举类型用来用户自定义一组常量值
    • 与C语言的枚举类型非常相似,对应整型值
  • 函数(function):一种表示函数的类型

Solidity引用类型

数组(Array)

  • 数组可以在声明时指定长度(定长数组),也可以动态调整大小(变长数组、动态数组)
  • 对于存储型(storage) 的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体);对于内存型(memory)的数组来说,元素类型不能是映射(mapping)类型
  • 字符数组(Byte Arrays)
    • 定长字符数组
      • 属于值类型,bytes1,bytes2,…,bytes32分别代表了长度为1到32的字节序列
      • 有一个.length属性,返回数组长度(只读)
    • 变长字符数组
      • 属于引用类型,包括 bytes和string,不同的是bytes是Hex字符串,而string 是UTF-8编码的字符串
  • 固定大小k和元素类型T的数组被写为T [k],动态大小的数组为T []。例如,一个由5个uint动态数组组成的数组是uint [][5]
  • 要访问第三个动态数组中的第二个uint,可以使用x [2] [1]
  • 越界访问数组,会导致调用失败回退
  • 如果要添加新元素,则必须使用.push()或将.length增大
  • 变长的storage数组和bytes(不包括string)有一个push()方法。可以将一个新元素附加到数组末端,返回值为当前长度

结构(Struct)

  • Solidity 支持通过构造结构体的形式定义新的类型
  • 结构类型可以在映射和数组中使用,它们本身可以包含映射和数组
  • 结构不能包含自己类型的成员,但可以作为自己数组成员的类型,也可以作为自己映射成员的值类型

映射(Mapping)

  • 映射可以视作哈希表 ,在实际的初始化过程中创建每个可能的key, 并将其映射到字节形式全是零的值(类型默认值)
  • 声明一个映射:mapping(_KeyType => _ValueType)
  • _KeyType可以是任何基本类型。这意味着它可以是任何内置值类型加上字节和字符串。不允许使用用户定义的或复杂的类型,如枚举,映射,结构以及除bytes和string之外的任何数组类型
  • _ValueType可以是任何类型,包括映射

Solidity地址类型

address

  • 地址类型存储一个 20 字节的值(以太坊地址的大小);地址类型也有成员变量,并作为所有合约的基础

address payable(v0.5.0引入)

  • 与地址类型基本相同,不过多出了transfersend两个成员变量

两者区别和转换

  • Payable 地址是可以发送 ether 的地址,而普通 address 不能
  • 允许从 payable address 到 address 的隐式转换,而反过来的直接转换是不可能的(唯一方法是通过uint160来进行中间转换)
  • 从0.5.0版本起,合约不再是从地址类型派生而来,但如果它有payable的回退函数,那同样可以显式转换为 address 或者 address payable 类型
地址类型成员变量

<address>.balance (uint256)

  • 该地址的 ether 余额,以Wei为单位

<address payable>.transfer(uint256 amount)

  • 向指定地址发送数量为amount的 ether(以Wei为单位),失败时抛出异常,发送 2300 gas 的矿工费,不可调节

<address>.send(uint256 amount) returns (bool)

  • 向指定地址发送数量为amount的 ether(以Wei为单位),失败时返回false,发送 2300 gas 的矿工费用,不可调节

<address>.call(bytes memory) returns (bool, bytes memory)

  • 发出底层函数CALL,失败时返回false,发送所有可用gas,可调节

<address>.delegatecall(bytes memory) returns (bool, bytes memory)

  • 发出底层函数DELEGATECALL,失败时返回 false,发送所有可用gas,可调节

<address>.staticcall(bytes memory) returns (bool, bytes memory)

  • 发出底层函数STATICCALL ,失败时返回false,发送所有可用gas,可调节
地址成员变量用法

balancetransfer

  • 可以使用balance属性来查询一个地址的余额
  • 可以使用transfer函数向一个payable地址发送以太币Ether(以 wei 为单位)

send

  • sendtransfer的低级版本。如果执行失败,当前的合约不会因为异常而终止,但send会返回 false

call

  • 也可以用call来实现转币的操作,通过添加.gas().value()修饰器

Solidity数据位置

  • 所有的复杂类型,即数组结构映射类型,都有一个额外属性——“数据位置”,用来说明数据是保存在内存memory中还是存储在storage
  • 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字storagememory进行修改
  • 函数参数(包括返回的参数)的数据位置默认是memory, 局部变量的数据位置默认是storage,状态变量的数据位置强制是storage
  • 另外还存在第三种数据位置——calldata ,这是一块只读的,且不会永久存储的位置,用来存储函数参数。 外部函数的参数(非返回参数)的数据位置被强制指定为calldata,效果跟memory差不多
  • 数据位置总结
    • 强制指定的数据位置
      • 外部函数的参数(不包括返回参数):calldata
      • 状态变量:storage
    • 默认数据位置
      • 函数参数(包括返回参数):memory
      • 引用类型的局部变量: storage
      • 值类型的局部变量:栈(stack
    • 特别要求
      • 公开可见(publicly visible)的函数参数一定是memory类型,如果要求是storage类型则必须是private或者internal函数,这是为了防止随意的公开调用占用资源

Solidity函数

  • 函数的值类型有两类:
    • 内部(internal)函数
    • 外部(external) 函数
  • 内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数
  • 外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回
  • 调用内部函数:直接使用名字f
  • 调用外部函数:this.f(当前合约),a.f(外部合约)

Solidity函数可见性

函数的可见性可以指定为 external,public ,internal 或者 private;对于状态变量,不能设置为 external ,默认是 internal

  • external :外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率
  • public :public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数
  • internal :这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用
  • private :private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用

Solidity函数状态可变性

  • pure:纯函数,不允许修改或访问状态
  • view:不允许修改状态
  • payable:允许从消息调用中接收以太币Ether
  • constant:与view相同,一般只修饰状态变量,不允许赋值(除初始化以外)

以下情况被认为是修改状态:

  • 修改状态变量
  • 产生事件
  • 创建其它合约
  • 使用 selfdestruct
  • 通过调用发送以太币
  • 调用任何没有标记为 view 或者 pure 的函数
  • 使用低级调用
  • 使用包含特定操作码的内联汇编

以下被认为是从状态中进行读取:

  • 读取状态变量
  • 访问 this.balance 或者 .balance
  • 访问 block,tx, msg中任意成员 (除 msg.sig 和 msg.data 之外)
  • 调用任何未标记为 pure 的函数
  • 使用包含某些操作码的内联汇编

函数修饰器(modifier)

  • 使用修饰器modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修饰器modifier 是合约的可继承属性, 并可能被派生合约覆盖
  • 如果同一个函数有多个修饰器modifier,它们之间以空格隔开,修饰器modifier 会依次检查执行

回退函数(fallback)

  • 回退函数(fallback function)是合约中的特殊函数;没有名字,不能有参数也不能有返回值
  • 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行
  • 每当合约收到以太币(没有任何数据),回退函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。 如果不存在这样的函数,则合约不能通过常规交易接收以太币
  • 在上下文中通常只有很少的 gas 可以用来完成回退函数的调用,所以使 fallback 函数的调用尽量廉价很重要

事件(event)

  • 事件是以太坊EVM提供的一种日志基础设施。事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等
  • 当定义的事件触发时,我们可以将事件存储到EVM的交易日志中,日志是区块链中的一种特殊数据结构;日志与合约关联,与合约的存储合并存入区块链中;只要某个区块可以访问,其相关的日志就可以访问;但在合约中,我们不能直接访问日志和事件数据
  • 可以通过日志实现简单支付验证 SPV(Simplified Payment Verification),如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中

Solidity异常处理

  • Solidity使用“状态恢复异常”来处理异常。这样的异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且向调用者返回错误
  • 函数assert和require可用于判断条件,并在不满足条件时抛出异常
  • assert() 一般只应用于测试内部错误,并检查常量
  • require() 应用于确保满足有效条件(如输入或合约状态变量),或验证调用外部合约的返回值
  • revert() 用于抛出异常,它可以标记一个错误并将当前调用回退

Solidity中的单位

  • 以太币 Ether 单位之间的换算就是在数字后边加上 wei、 finney、 szabo 或 ether 来实现的,如果后面没有单位,缺省为 Wei。例如 2 ether == 2000 finney 的逻辑判断值为 true

秒是缺省时间单位,在时间单位之间,数字后面带 有 seconds、 minutes、 hours、 days、 weeks 和 years 的可以进行换算,基本换算关系如下:

  • 1 == 1 seconds
  • 1 minutes == 60 seconds
  • 1 hours == 60 minutes
  • 1 days == 24 hours
  • 1 weeks == 7 days
  • 1 years == 365 days

简单投票DApp

主要内容:ganache、简单投票DApp

  1. 我们首先安装一个叫做ganache的模拟区块链,能够让我们的程序在开发环境中运行
  2. 写一个合约并部署到ganache
  3. 然后我们会通过命令行和网页与ganache进行交互

此处具体代码分析留至以后用到以太坊DApp再写……

web3.js及简单应用

主要内容:web3.js API、转币脚本、监听脚本

web3.js

  • Web3 JavaScript app API
  • web3.js 是一个JavaScript API库。要使DApp在以太坊上运行,我们可以使用web3.js库提供的web3对象
  • web3.js 通过RPC调用与本地节点通信,它可以用于任何暴露了RPC层的以太坊节点
  • web3 包含 eth 对象 - web3.eth(专门与以太坊区块链交互)和 shh 对象 - web3.shh(用于与 Whisper 交互)
  • 我们与区块链进行通信的方式是通过 RPC(Remote Procedure Call)。web3js 是一个 JavaScript 库,它抽象出了所有的 RPC 调用,以便于你可以通过 JavaScript 与区块链进行交互。另一个好处是,web3js 能够让你使用你最喜欢的 JavaScript 框架构建非常棒的 web 应用

web3 模块加载

  • 首先需要将 web3 模块安装在项目中
  • 然后创建一个 web3 实例,设置一个“provider”
  • 为了保证我们的 MetaMask 设置好的 provider 不被覆盖掉,在引入 web3 之前我们一般要做当前环境检查

异步回调(callback)

  • web3js API 设计的最初目的,主要是为了和本地 RPC 节点共同使用,所以默认情况下发送的是同步 HTTP 请求
  • 如果要发送异步请求,可以在函数的最后一个参数位置上,传入一个回调函数。回调函数是可选(optioanl)的
  • 我们一般采用的回调风格是所谓的“错误优先”

回调 Promise 事件

  • 为了帮助 web3 集成到不同标准的所有类型项目中,1.0.0 版本提供了多种方式来处理异步函数。 大多数的 web3 对象允许将一个回调函数作为最后一个函数参数传入,同时会返回一个 promise 用于链式函数调用
  • 以太坊作为一个区块链系统,一次请求具有不同的结束阶段。为了满足这样的要求,1.0.0 版本将这类函数调用的返回值包成一个“承诺事件”(promiEvent),这是一个 promise 和 EventEmitter 的结合体
  • PromiEvent 的用法就像 promise 一样,另外还加入了.on,.once 和.off方法

应用二进制接口(ABI)

  • web3.js 通过以太坊智能合约的 json 接口(Application Binary Interface,ABI)创建一个 JavaScript 对象,用来在 js 代码中描述
  • 函数(functions)
    • type:函数类型,默认“function”,也可能是“constructor”
    • constant, payable, stateMutability:函数的状态可变性
    • inputs, outputs: 函数输入、输出参数描述列表
  • 事件(events)
    • type:类型,总是“event”
    • inputs:输入对象列表,包括 name、type、indexed

批处理请求(batch requests)

  • 批处理请求允许我们将请求排序,然后一起处理它们
  • 注意:批量请求不会更快。实际上,在某些情况下,一次性地发出许多请求会更快,因为请求是异步处理的
  • 批处理请求主要用于确保请求的顺序,并串行处理

大数处理(big numbers)

  • JavaScript 中默认的数字精度较小,所以web3.js 会自动添加一个依赖库 BigNumber,专门用于大数处理
  • 对于数值,我们应该习惯把它转换成 BigNumber 对象来处理
  • BigNumber.toString(10) 对小数只保留20位浮点精度。所以推荐的做法是,我们内部总是用 wei 来表示余额(大整数),只有在需要显示给用户看的时候才转换为ether或其它单位

常用 API —— 基本信息查询

查看 web3 版本

  • v0.2x.x:web3.version.api
  • v1.0.0:web3.version

查看 web3 连接到的节点版本(clientVersion)

  • 同步:web3.version.node
  • 异步: web3.version.getNode((error,result)=>{console.log(result)})
  • v1.0.0:web3.eth.getNodeInfo().then(console.log)

基本信息查询

获取 network id

  • 同步:web3.version.network
  • 异步:web3.version.getNetwork((err, res)=>{console.log(res)})
  • v1.0.0:web3.eth.net.getId().then(console.log)

获取节点的以太坊协议版本

  • 同步:web3.version.ethereum
  • 异步:web3.version.getEthereum((err, res)=>{console.log(res)})
  • v1.0.0:web3.eth.getProtocolVersion().then(console.log)

网络状态查询

是否有节点连接/监听,返回true/false

  • 同步:web3.isConnect() 或者 web3.net.listening •
  • 异步:web3.net.getListening((err,res)=>console.log(res))
  • v1.0.0:web3.eth.net.isListening().then(console.log)

查看当前连接的 peer 节点

  • 同步:web3.net.peerCount
  • 异步:web3.net.getPeerCount((err,res)=>console.log(res))
  • v1.0.0:web3.eth.net.getPeerCount().then(console.log) Provider

查看当前设置的 web3 provider

  • web3.currentProvider

查看浏览器环境设置的 web3 provider(v1.0.0)

  • web3.givenProvider

设置 provider

  • web3.setProvider(provider)
    • web3.setProvider(new web3.providers.HttpProvider(‘http://localhost:8545’))

web3 通用工具方法

以太单位转换

  • web3.fromWei web3.toWei

数据类型转换

  • web3.toString web3.toDecimal web3.toBigNumber

字符编码转换

  • web3.toHex web3.toAscii web3.toUtf8 web3.fromUtf8

地址相关

  • web3.isAddress web3.toChecksumAddress

web3.eth – 账户相关

coinbase 查询

  • 同步:web3.eth.coinbase
  • 异步:web3.eth.getCoinbase( (err, res)=>console.log(res) )
  • v1.0.0:web3.eth.getCoinbase().then(console.log)

账户查询

  • 同步:web3.eth.accounts
  • 异步:web3.eth.getAccounts( (err, res)=>console.log(res) )
  • v1.0.0:web3.eth.getAccounts().then(console.log)

区块相关

区块高度查询

  • 同步:web3.eth. blockNumber
  • 异步:web3.eth.getBlockNumber( callback )

gasPrice 查询

  • 同步:web3.eth.gasPrice
  • 异步:web3.eth.getGasPrice( callback )

区块相关

区块查询

  • 同步:web3.eth.getBlockNumber( hashStringOrBlockNumber [ ,returnTransactionObjects] )
  • 异步:web3.eth.getBlockNumber( hashStringOrBlockNumber, callback )

块中交易数量查询

  • 同步: web3.eth.getBlockTransactionCount( hashStringOrBlockNumber )
  • 异步: web3.eth.getBlockTransactionCount( hashStringOrBlockNumber , callback )

交易相关

余额查询

  • 同步:web3.eth.getBalance(addressHexString [, defaultBlock])
  • 异步:web3.eth.getBalance(addressHexString [, defaultBlock] [, callback])

交易查询

  • 同步:web3.eth.getTransaction(transactionHash)
  • 异步:web3.eth.getTransaction(transactionHash [, callback])

交易执行相关

交易收据查询(已进块)

  • 同步:web3.eth.getTransactionReceipt(hashString)
  • 异步:web3.eth.getTransactionReceipt(hashString [, callback])

估计 gas 消耗量

  • 同步:web3.eth.estimateGas(callObject)
  • 异步:web3.eth.estimateGas(callObject [, callback])

发送交易

  • web3.eth.sendTransaction(transactionObject [, callback])
  • 交易对象:
    • from:发送地址
    • to:接收地址,如果是创建合约交易,可不填
    • value:交易金额,以wei为单位,可选
    • gas:交易消耗 gas 上限,可选
    • gasPrice:交易 gas 单价,可选
    • data:交易携带的字串数据,可选
    • nonce:整数 nonce 值,可选

消息调用

web3.eth.call(callObject [, defaultBlock] [, callback])

参数:

  • 调用对象:与交易对象相同,只是from也是可选的
  • 默认区块:默认“latest”,可以传入指定的区块高度
  • 回调函数,如果没有则为同步调用

日志过滤(事件监听)

  • web3.eth.filter( filterOptions [ , callback ] )

合约相关 —— 创建合约

  • web3.eth.contract

调用合约函数

  • 可以通过已创建的合约实例,直接调用合约函数

监听合约事件

  • 合约的 event 类似于 filter,可以设置过滤选项来监听

深入理解合约工作流

自动化编译和部署

主要内容:编写编译脚本和部署脚本

编译是对合约进行部署和测试的前置步骤,编译步骤的目标是把源代码转成 ABI 和 Bytecode,并且能够处理编译时抛出的错误,确保不会在包含错误的源代码上进行编译,我们可以将编译过程写成脚本,自动完成。我们通过编译从 solidity 源码得到了字节码,接下来我们完成一个自动化脚本,将合约部署到区块链网络中

此处具体代码分析留至以后用到以太坊DApp再写……

自动化测试

主要内容:Ganache

上面已经实现了合约的编译和部署的自动化,这将大大提升我们开发的效率。但流程的自动化并不能保证我们的代码质量。质量意识是靠谱工程师的基本职业素养,在智能合约领域也不例外:任何代码如果不做充分的测试,问题发现时通常都已为时太晚;如果代码不做自动化测试,问题发现的成本就会越来越高

在编写合约时,我们可以利用 remix 部署后的页面调用合约函数,进行单元测试;还可以将合约部署到私链,用 geth 控制台或者 node 命令行进行交互测试。但这有很大的随意性,并不能形成标准化测试流程;而且手动一步步操作,比较繁琐,不易保证重复一致。 于是我们想到,是否可以利用现成的前端技术栈实现合约的自动化测试呢? 当然是可以的,mocha就是这样一个 JavaScript 测试框架

进行单元测试,比较重要的一点是保证测试的独立性和隔离性,所以我们并不需要测试网络这种有复杂交互的环境,甚至不需要本地私链保存测试历史。而ganache基于内存模拟以太坊节点行为,每次启动都是一个干净的空白环境,所以非常适合我们做开发时的单元测试。还记得 ganache 的前身叫什么吗?就是大名鼎鼎的 testRPC

mocha 是 JavaScript 的一个单元测试框架,既可以在浏览器环境中运行,也可以在 node.js 环境下运行。我们只需要编写测试用例,mocha 会将测试自动运行并给出测试结果

mocha 的主要特点有:

  • 既可以测试简单的 JavaScript 函数,又可以测试异步代码;
  • 可以自动运行所有测试,也可以只运行特定的测试;
  • 可以支持 before、after、beforeEach 和 afterEach 来编写初始化代码

此处具体代码分析留至以后用到以太坊DApp再写……

合约工作流

主要内容:深入理解合约工作流

到目前为止,我们已经熟悉了智能合约的开发、编译、部署、测试,而在实际工作中,把这些过程串起来才能算作是真正意义上的工作流。比如修改了合约代码需要重新运行测试,但是重新运行测试之前需要重新编译,而部署的过程也是类似的,每次部署的都要是最新的合约代码。 通过 npm script 机制,我们可以把智能合约的工作流串起来,让能自动化的尽可能自动化

此处具体代码分析留至以后用到以太坊DApp再写……

深入理解以太坊原理

以太坊的理念与实现

主要内容:白皮书、黄皮书

以太坊白皮书概要性地介绍了以太坊,而以太坊黄皮书则通过大量的定义和公式详细地描述了以太坊的技术实现。

此处具体分析留至以后深入学习以太坊再写……

源码结构及分析

主要内容:代码结构、MPT、GHOST

MPT是什么

  • Merkel Patricia Tree (MPT),翻译为梅克尔-帕特里夏树
  • MPT 提供了一个基于密码学验证的底层数据结构,用来存储键值对(key-value)关系
  • MPT 是完全确定性的,这是指在一颗 MPT 上一组键值对是唯一确定的,相同内容的键可以保证找到同样的值,并且有同样的根哈希(root hash)
  • MPT 的插入、查找、删除操作的时间复杂度都是O(log(n)),相对于其它基于复杂比较的树结构(比如红黑树),MPT 更容易理解,也更易于编码实现

从字典树(Trie)说起

  • 字典树(Trie)也称前缀树(prefix tree),属于搜索树,是一种有序的树数据结构
  • 字典树用于存储动态的集合或映射,其中的键通常是字符串

基数树(Radix Tree)

基数树又叫压缩前缀树(compact prefix tree),是一种空间优化后的字典树,其中如果一个节点只有唯一的子节点,那么这个子节点就会与父节点合并存储

基数树节点

  • 在一个标准的基数树里,每个节点存储的数据如下: [i0, i1, … in, value]
  • 这里的 i0,i1,…,in 表示定义好的字母表中的字符,字母表中一共有n+1个字符,这颗树的基数(radix)就是 n+1
  • value 表示这个节点中最终存储的值
  • 每一个 i0 到 in 的“槽位”,存储的或者是null,或者是指向另一节点的指针
  • 用节点的访问路径表示 key,用节点的最末位置存储value,这就实现了一个基本的键值对存储

示例

  • 我们有一个键值对{ “dog”: “puppy” },现在希望通过键 dog 访问它的值;我们采用16进制的 Hex 字符作为字符集
  • 首先我们将 “dog” 转换成 ASCII 码,这样就得到了字符集中的表示 64 6f 67,这就是树结构中对应的键
  • 按照键的字母序,即 6->4->6->f->6->7,构建树中的访问路径
  • 从树的根节点(root)出发,首先读取索引值(index)为 6 的插槽中存储的值,以它为键访问到对应的子节点
  • 然后取出子节点索引值为 4 的插槽中的值,以它为键访问下一层节点,直到访问完所需要的路径
  • 最终访问到的叶子节点,就存储了我们想要查找的值,即“puppy”

基数树的问题

数据校验

基数树节点之间的连接方式是指针,一般是用32位或64位的内存地址作为指针的值,比如C语言就是这么做的。但这种直接存地址的方式无法提供对数据内容的校验,而这在区块链这样的分布式系统中非常重要

访问效率

基数树的另一个问题是低效。如果我们只想存一个 bytes32 类型的键值对,访问路径长度就是64(在以太坊定义的 Hex 字符集下);每一级访问的节点都至少需要存储 16 个字节,这样就需要至少 1k 字节的额外空间,而且每次查找和删除都必须完整地执行 64 次下探访问

梅克尔树(Merkel Tree)

  • 也被称作哈希树(Hash Tree),以数据块的 hash 值作为叶子节点存储值。梅克尔树的非叶子节点存储其子节点内容串联拼接后的 hash 值

帕特里夏树(Patricia Tree)

  • 如果一个基数树的“基数”(radix)为2或2的整数次幂,就被称为“帕特里夏树”,有时也直接认为帕特里夏树就是基数树
  • 以太坊中采用 Hex 字符作为 key 的字符集,也就是基数为16 的帕特里夏树
  • 以太坊中的树结构,每个节点可以有最多 16 个子节点,再加上 value,所以共有 17 个“插槽”(slot)位置
  • 以太坊中的帕特里夏树加入了一些额外的数据结构,主要是为了解决效率问题

MPT(Merkel Patricia Tree)

  • 梅克尔-帕特里夏树是梅克尔树和帕特里夏树的结合
  • 以太坊中的实现,对 key 采用 Hex 编码,每个 Hex 字符就是一个nibble(半字节)
  • 遍历路径时对一个节点只访问它的一个 nibble ,大多数节点是一个包含17个元素的数组;其中16个分别以 hex字符作为索引值,存储路径中下一个 nibble 的指针;另一个存储如果路径到此已遍历结束,需要返回的最终值。这样的节点叫做“分支节点”(branch node)
  • 分支节点的每个元素存储的是指向下一级节点的指针。与传统做法不同,MPT 是用所指向节点的 hash 来代表这个指针的;每个节点将下个节点的 hash 作为自己存储内容的一部分,这样就实现了 Merkel 树结构,保证了数据校验的有效性

MPT 节点分类

  • MPT 中的节点有以下几类:
    • 空节点(NULL)
      • 表示空字符串
    • 分支节点(branch)
      • 17 个元素的节点,结构为 [ v0 … v15, vt ]
    • 叶子节点(leaf)
      • 拥有两个元素,编码路径 encodedPath 和值 value
    • 扩展节点(extension)
      • 拥有两个元素,编码路径 encodedPath 和键 key

MPT 中数据结构的优化

  • 对于64个字符的路径长度,很有可能在某个节点处会发现,下面至少有一段路径没有分叉;这很难避免
  • 我们当然可以依然用标准的分支节点来表示,强制要求这个节点必须有完整的16个索引,并给没有用到的那15个位置全部赋空值;但这样有点蠢
  • 通过设置“扩展节点”,就可以有效地缩短访问路径,将冗长的层级关系压缩成一个键值对,避免不必要的空间浪费
  • 扩展节点(extension node)的内容形式是 [encodedPath, key],其中 encodedPath 包含了下面不分叉的那部分路径,key 是指向下一个节点的指针(hash,也即在底层db中的存储位置)
  • 叶子节点(leaf node):如果在某节点后就没有了分叉路径,那这是一个叶子节点,它的第二个元素就是自己的 value

紧凑编码(compact coding)

  • 路径压缩的处理相当于实现了压缩前缀树的功能;不过路径表示是 Hex 字符串(nibbles),而存储却是以字节(byte)为单位的,这相当于浪费了一倍的存储空间 •
  • 我们可以采用一种紧凑编码(compact coding)方式,将两个 nibble 整合在一个字节中保存,这就避免了不必要的浪费
  • 这里就会带来一个问题:有可能 nibble 总数是一个奇数,而数据总是以字节形式存储的,所以无法区分 nibble 1 和nibbles 01;这就使我们必须分别处理奇偶两种情况
  • 为了区分路径长度的奇偶性,我们在 encodedPath 中引入标识位

Hex 序列的压缩编码规则

  • 我们在 encodedPath 中,加入一个 nibble 作为前缀,它的后两位用来标识节点类型和路径长度的奇偶性

  • MPT 中还有一个可选的“结束标记”(用T表示),值为0x10 (十进制的16),它仅能在路径末尾出现,代表节点是一个最终节点(叶子节点)
  • 如果路径是奇数,就与前缀 nibble 凑成整字节;如果是偶数,则前缀 nibble 后补 0000 构成整字节

编码示例

  • [ 1, 2, 3, 4, 5, …] 不带结束位,奇路径
    • ‘11 23 45’
  • [ 0, 1, 2, 3, 4, 5, …] 不带结束位,偶路径
    • ‘00 01 23 45’
  • [ 0, f, 1, c, b, 8, 10] 带结束位 T 的偶路径
    • ‘20 0f 1c b8’
  • [ f, 1, c, b, 8, 10] 带结束位 T 的奇路径
    • ‘3f 1c b8’

以太坊中树结构

  • 以太坊中所有的 merkel 树都是 MPT
  • 在一个区块的头部(block head)中,有三颗 MPT 的树根:
    • stateRoot
      • 状态树的树根
    • transactionRoot
      • 交易树的树根
    • receiptsRoot
      • 收据树的树根

以太坊中树结构

  • 状态树(state trie)
    • 世界状态树,随时更新;它存储的键值对 (path, value) 可以表示为(sha3(ethereumAddress), rlp(ethereumAccount) )
    • 这里的 account 是4个元素构成的数组:[nonce, balance, storageRoot, codeHash]
  • 存储树(storage trie)
    • 存储树是保存所有合约数据的地方;每个合约账户都有一个独立隔离 的存储空间
  • 交易树(transaction trie)
    • 每个区块都会有单独的交易树;它的路径(path)是 rlp(transactionIndex),只有在挖矿时才能确定;一旦出块,不再更改
  • 收据树(receipts trie)
    • 每个区块也有自己的收据树;路径也表示为 rlp(transactionIndex)

DApp项目实战

以下三个项目留至以后深入学习以太坊DApp再写……

基于token的投票

主要内容:Truffle、加入token的合约

基于ipfs的去中心化eBay

主要内容:IPFS、多合约交互

ICO DApp

主要内容:next.js、react、material-UI、mocha

参考资料

Built with Hugo
Theme Stack designed by Jimmy