1、以太坊概述
1.1 简介
以太坊作为区块链2.0的典型代表,是为了解决比特币浪费电能、网络拥堵等问题而重新设计的公链。
大家可以查看eth中文社区eth-fans提供的星火节点(https://stats.ethfans.org),星火节点类似比特币的种子节点,星火节点的信息会被打包到节点文件中供社区成员下载,通过使用节点文件,本地运行的以太坊客户端可以连接到更多的超级节点,大大加快了区块同步速度。
1.2 以太坊架构
以太坊总体架构:
[图片上传失败...(image-59a5d1-1533223786890)]
上图简要描述了以太的软件架构,可以看出是EVM和智能合约扩展了上层应用程序,使之成为区块链2.0.
区块链六层架构:
[图片上传失败...(image-264659-1533223468139)]
数据层:是一个区块 + 链表的数据结构,本质是一个分布式区块链。
网络层:p2p网络。
共识层:制定区块链的获取货币的机制。比如比特币用的是POW(Proof of Work工作量证明机制):电脑的性能越好,越容易获取到货币奖励。还有POS(Proof of Stake权益证明机制):类似于众筹分红的概念,会根据你持有的货币数量和时间,给持有者发放利息。还有比如超级账本用的是PBFT(拜赞庭容错)。
激励层:挖矿机制
合约层:以往的区块链是没有这一层的。所以最初的区块链只能进行交易,而无法用于其他的领域或是进行其他的逻辑处理。但是合约层的出现,使得在其他领域使用区块链成为了现实,比如用于IOT。以太坊中这部分包括了EVM(以太坊虚拟机)和智能合约两部分。
应用层:区块链的展示层。如以太坊使用的是truffle和web3-js.区块链的应用层可以是移动端,web端,或是是融合进现有的服务器,把当前的业务服务器当成应用层。
1.3 以太坊常用项目介绍
以太坊的源码托管在https://github.com/ethereum
由上图可以看到以太坊有两种语言写的客户端,其中go-ethereum
是官方首推的版本,cpp
是用c++写的版本,两个版本功能完全一样,只是不同语言实现的而已。
除了以上的核心客户端外,以太坊还提供了一系列独立的工具,例如还在实验中的智能合约编程语言Viper、Solidity,和以太坊的JS调用库Web3.js,以太坊官方钱包等。目前eth的github
官网已经有150多个各类功能的工具项目,以下整理一些常用的进行说明:
go-ethereum
官方go语言客户端,客户端文件是geth
。这是使用最广泛的客户端,类似比特币的中本聪客户端,可用于挖矿、组建私有链,管理帐号、部署智能合约等。但是不能编译只能合约(1.6之前还是内置编译模块的,1.6之后就独立出去了)。该客户端可以作为一个独立程序运行,也可以作为一个库文件嵌入其他的GO、Android和ios项目中,它没有界面,是一个命令行程序。
- cpp-ethereum
与第一个相同,只是用C++实现的。
- EIPs
官方描述是eth平台的实施标准,包括核心协议详述,客户端API及合约的标准。
以上三项也是官方github置顶的三个项目。
- mist
JS写的一个DAPP browser客户端,可以用于浏览和使用DAPP,类似app strore,此前也是钱包客户端,其实可以把Ethereum Wallet
理解为配置Mist Browser上的一个dapp
。
MIST一般是配合go-ethereum
或者cpp-ethereum
运行的,如果在Mist启动是没有运行一个命令行的ethereum
客户端,则mist将启动区块链数据同步(通常绑定geth
客户端)。如果想要Mist运行在一个私有网络,只要在Mist启动前先启动节点(即geth
)即可,Mist可以通过IPC链接到私有链。
- Solidity项目。
Solidity使用C++开发,客户端文件为solc,跨平台,使用命令行界面。solc是一个基本的编译平台,Solidity是以太坊智能合约的编程语言。
- browse-solidity项目
智能合约浏览器版本的IDE,可以直接在浏览器中进行开发、调试、编译。
- Remix
智能合约(以太坊称为DAPP)开发IDE,采用图形化界面可以支持DAPP的编写、调试、部署,是目前最主流的以太坊智能合约开发平台。Remix现在可以和brower Solidity集成在一起使用了。
- pyethereum项目
Python语言编写的以太坊客户端。
- ethereumj项目
Java语言编写的客户端,与geth功能完全相同。
1.4 以太坊目录简介
accounts 实现了一个高等级的以太坊账户管理
bmt 二进制的默克尔树的实现
build 主要是编译和构建的一些脚本和配置
cmd 命令行工具,又分了很多的命令行工具,下面一个一个介绍
/abigen Source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages
/bootnode 启动一个仅仅实现网络发现的节点
/evm 以太坊虚拟机的开发工具, 用来提供一个可配置的,受隔离的代码调试环境
/faucet
/geth 以太坊命令行客户端,最重要的一个工具
/p2psim 提供了一个工具来模拟http的API
/puppeth 创建一个新的以太坊网络的向导
/rlpdump 提供了一个RLP数据的格式化输出
/swarm swarm网络的接入点
/util 提供了一些公共的工具
/wnode 这是一个简单的Whisper节点。 它可以用作独立的引导节点。此外,可以用于不同的测试和诊断目的。
common 提供了一些公共的工具类
compression Package rle implements the run-length encoding used for Ethereum data.
consensus 提供了以太坊的一些共识算法,比如ethhash, clique(proof-of-authority)
console console类
contracts
core 以太坊的核心数据结构和算法(虚拟机,状态,区块链,布隆过滤器)
crypto 加密和hash算法,
eth 实现了以太坊的协议
ethclient 提供了以太坊的RPC客户端
ethdb eth的数据库(包括实际使用的leveldb和供测试使用的内存数据库)
ethstats 提供网络状态的报告
event 处理实时的事件
les 实现了以太坊的轻量级协议子集
light 实现为以太坊轻量级客户端提供按需检索的功能
log 提供对人机都友好的日志信息
metrics 提供磁盘计数器
miner 提供以太坊的区块创建和挖矿
mobile 移动端使用的一些warpper
node 以太坊的多种类型的节点
p2p 以太坊p2p网络协议
rlp 以太坊序列化处理
rpc 远程方法调用
swarm swarm网络处理
tests 测试
trie 以太坊重要的数据结构Package trie implements Merkle Patricia Tries.
whisper 提供了whisper节点的协议。
1.5 以太坊关键概念
1.5.1 状态
以太坊中将状态转换的过程称为状态转换函数‘。
| 状态1 | 状态转换函数---> | 状态2 |
以太坊的每一个区块头,都包含了指向三棵树的指针,分别是:状态树、交易树、收据树。
交易树指针就类似于比特比区块头中的梅克尔树根,交易树是用来代表区块中发生的所有交易历史的;
状态树:代表访问区块后的整个状态;
收据树代表每笔交易对应的收据,所谓的收据是指每一笔交易影响的数据条,或者说是每一笔交易影响的结果。这些都是针对比特比中梅克尔交易树的增强,通过状态树可以很方便地获得类似账户存在与否、账户余额、订单状态这样的结果,而不用只依靠交易事务去追溯。
以太坊状态树的示意图:
[图片上传失败...(image-b80dae-1533223468140)]
如上图所示,状态树中存储了整个系统的状态数据,比如账户余额、合约存储、合约代码以及账户随机数等数据。状态树有点类似我们玩游戏时的存档功能。
再来看以下源码中如何定义状态树根哈希的:
go-ethereum/core/types/block.go: 以太坊的区块头结构
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time *big.Int `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash" gencodec:"required"`
Nonce BlockNonce `json:"nonce" gencodec:"required"`
}
root : 状态树根哈希
TxHash:交易树根哈希
ReceiptHash:收据树根哈希
1.5.2 账户
在以太坊系统中,状态是由被称为“账户”的对象和在两个账户之间转移价值和信息的状态转换构成的,每个账户有一个20字节的地址,这个其实就跟银行账户差不多意思。 注意,比特币中是没有账户的概念的。
以太坊中由于有账户的概念,所以可直接获得当前的余额。
看以下代码中如何定义的:
go-ethereum/core/state/state_object.go中
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
由代码可以看到,账户是由四个部分组成:
Nonce:随机数,用于确定每笔交易只能被处理一次的计数器,实际上就是每个账户的交易计数,用以防止重放攻击,当一个账户发送一笔交易时,根据已经发送的交易数来累加这个数字,比如账户发送了5个交易,则账户随机数是5.
Balance:账户目前的以太币余额
Root:账户的存储(默认为空,指向的是一颗帕夏尔前缀树
CodeHash:账户的合约代码(只有合约账户才有,否则为空)
以太坊中的账户是区分类型的:
1、外部账户
EOA,就是一般账户,外部所有账户是由一堆密钥定义的,一个私钥一个公钥,公钥的后20位作为其地址(跟比特比类似)。外部所有账户是没有代码的,但是可以通过创建和签名一笔交易从一个外部账户发送消息到合约账户,通过传递一些参数,如EOA的地址,合约的地址,以及数据,使用ABI作为传递数据的编码和解码的标准。
2、合约账户
合约账户是一种特殊的可编程账户,合约账户可以在账户间传递消息,合约存储在以太坊的区块链上,并被编译为以太坊虚拟字节码,合约账户也是有地址的,不过与外部所有账户不同,不是根据公钥来获得的,而是通过合约创建者的地址和该地址发出过的交易数量计算得到。
外部所有账户在以太坊中相当于一把钥匙,合约账户相当于一个籍贯,一旦被外部所有账户确认激活,机关就启动了。
1.5.3 交易
以太坊中的交易也就是上一节说到的状态转换过程。
在以太坊中交易是一个广义的概念,它的定义是指签名的数据包,这个数据包中存储了从外部账户发送的消息。
所谓的交易就是一个消息,这个消息被发送者签名了。
以太坊中的交易源码:
core/types/transaction.go
type Transaction struct {
data txdata
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
其中的data是主要的字段,是结构体类型:
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
1)AccountNonce:表明交易发送者已发送过的交易数,与账户结构中定义的随机数对应;
2)Price与GasLimit:单价和所需的计算量,它俩的乘积是总的花费,用来抵抗DDOS攻击。
3)Recipient:接收方的地址;
4)Amount:发送的以太币余额,单位是wei;
5)Payload:交易携带的数据,根据不同的交易类型有不同的用法;
6)V、R、S:交易的签名数据;
上面没有发送方地址,是因为它可以通过签名获得。
以太坊中的3种交易类型:
1)转账交易
web3.eth.sendTransaction({from:"",to:"",value:}); //web3.js是一个JS库,可以通过RPC调用与本地节点通信,实际上就是一个外部应用程序用来调用以太坊核心节点功能的一个调用库。
2)合约创建交易
3)合约执行交易
交易数据在以太坊中是存在交易树中的,如1.5.1节中图所示,交易树非常类似BTC中的默克尔树,只不过存储的结构与编码方式有些差别。
1.5.4 收据
收据这个概念也是以太坊中特有的。
源码:
core/types/receipt.go
type Receipt struct {
// Consensus fields
PostState []byte `json:"root"`
Status uint64 `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields (don't reorder!)
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
}
交易执行后会影响状态的变更,会消耗Gas,我们来看下上述结构体中的主要属性:
1)PostState
:这是状态树的根哈希,不过不是直接存储的哈希值,而是转换为字节码存储,通过这个字段使得通过收据可以直接访问到状态数据。
2)CumulativeGasUsed
:所在区块的gas交易之和
3)TxHash
:交易事务的哈希值
4)ContractAddress
:合约地址,如果是普通的转账交易则为空
5)GasUsed
:本条交易消耗的Gas。
收据实际上是一个数据的统计记录,记录了交易执行后的特征数据。收据和交易是关联对应的。
其实从技术上来说收据不是必须的,没有它也可以查询到一些数据。它的存在只是为了提高账本数据在各种需求下的统计效率,方便各种查询。
1.5.5 RLP编码
RLP(Recursive length prifix),递归前缀编码,是一种数据编码方式,在以太坊中使用非常普遍,是以太坊中对象序列化的主要方式,在区块、交易、账户状态等地方都有使用。
比如交易从一个节点发送到另一个节点时,要被编译为一种特别的数据结构,这种结构称为trie树,也称为前缀树,然后根据这个前缀树计算出一个根哈希(上述的交易树,状态树,收据树都是这种方式),而这颗树的每一个数据项都会使用RLP的方式编码。
编码:将二进制数据按照某种格式和规则进行组装,以便于数据传送的编码与解码。
RLP编码规则:略
1)单字节编码
2)字符串长度是0-55字节
3)字符串长度大于55字节
以上都是字符串的编码,以太坊中还涉及到列表的编码,网上文档很多,此处不赘述。
1.5.6 默克尔-帕特里夏树
我们都知道比特币中有默克尔树的概念,每个区块头都有默克尔根,实际上就是一个区块中交易哈希树的根哈希值,而以太坊的区块头中有三个根哈希(交易、状态、收据),对应各自的树结构。那么区别是什么呢?
严格来说,比特币中的默克尔树叫二叉默克尔树,而以太坊中是默克尔-帕特里夏树(Merkle-Patricia Tree
,有时也称为帕特里夏树),是一种更加复杂的结构,它是默克尔树和帕特里夏树的结合。
我们来了解下什么是帕特里夏树.
1)Patricia Tree
应用场景:以交易数据为例,当交易数据从一个节点发送到另一个节点的时候,必须被编译为trie(前缀树),这个trie中的每一个项都使用RLP编码。在p2p网络上传输的交易是一个简单的列表,它们被组装成一个叫做trie树的特殊数据结构来计算根哈希,这意味着交易列表在本地以trie树的形式存储,发送给客户端的时候序列化成列表。这里的trie结构就是patiricia tree。
Ptricia Tree是基于trie tree的一种结构,trie tree是一种单词查找树结构。如下图:
[图片上传失败...(image-d34f89-1533223468140)]
trie中每个节点存储一个字符,这样能很好地节约存储空间。
而Ptiricia Tree每个节点可以存储字符串或者二进制串,这样就使其可以存储更为一般化的数据,而不只是一个单词字符:
[图片上传失败...(image-974cd-1533223468140)]
在这样的树结构中每个节点存储一个key-value键值对数据,key用来保存索引,是用来搜索定位的,value则是节点中具体的业务数据。
而以太坊中的树结构在这个基础上还要复杂不少,我们来看看以太坊中的Merkle Patricia Tree
具体是哪种结构。
- 以太坊中用的
Merkle Patricia Tree
- 首先以太坊中节点分为空节点、叶子节点、扩展节点、分支节点。树是由节点组成的,每个节点都是key-value形式,这些节点数据被存在本地的
LevelDB
中, 以键值方式存储,value是节点的RLP编码,key则是RLP编码的哈希值。
Node=(key,value) //节点中
对应LevelDB
中:
value=RLP(Node)
key=sha3(value)
以太坊网络中的核心客户端会不断地同步更新这个数据库以报错与网络中的其他客户端数据同步,源码中的定义:
trie/sync.go:
type SyncResult struct {
Hash common.Hash // Hash of the originally unknown trie node
Data []byte // Data content of the retrieved node
}
- 节点类型
1)空节点:value中是一个空字符串
2)叶子节点:
[图片上传失败...(image-e82a7d-1533223616512)]
3)扩展节点
[图片上传失败...(image-815000-1533223616512)]
注意,扩展节点指向的是存储在LevelDB
中的叶子节点哈希值。
4)分支节点
分支节点是真正存储业务数据的。Merkle Partricia Tree
作为一种前缀树,主要特点是依靠共享的前缀来提高树结构的处理性能,那么这个前缀就很重要了,对于扩展节点的和叶子节点来说,节点key就是起到前缀的作用。
[图片上传失败...(image-7b25f4-1533223616512)]
5)树结构示例
由于比特币只支持转账交易合约,所以默克尔树是一颗二叉哈希树,但以太坊支持智能合约,也增加了更多的概念,为了更有效的增删改查并且让树的结构更加平衡有效,因此设计了更复杂的结构:
[图片上传失败...(image-fca30-1533223616512)]
- 叶子节点1的前缀为01234,因为是由根节点01开始。就是要通过共享前缀来充分提高存取效率。
- 十六进制前缀
- 由于分支节点的结构是长度为17的列表很容易判断,叶子节点和扩展节点都是长度为2的字节,是靠在key的前面增加4位二进制前缀来识别的。
- 最低位用来表示key长度的奇偶性,第二低位用来表示是否终止(1表示终止,也就是叶子节点,0表示扩展节点)
[图片上传失败...(image-1f4e08-1533223616512)]
[图片上传失败...(image-e8d380-1533223616512)]
注意
增加的16进制前缀并不是属于节点key的一部分,而仅仅是在构建树结构的时候附加上去的。整棵树表示的数据在网络中传递时候就是一个列表数据,而树结构是以太坊客户端接收到数据后另行构造出来的。