汪晓明对区块链、以太坊的思考

记录创业、生活的所思所感,探讨去中心化思想,推动区块链的发展。

HPB-Wallet:HPB钱包助记词生成和备份

助记词生成

1 BIP32, BIP39, BIP44

  • BIP32:定义 Hierarchical Deterministic wallet (简称 “HD Wallet”),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等
  • BIP39:将 seed 用方便记忆和书写的单字表示。一般由 12 个单词组成,称为 mnemonic code(phrase),中文称为助记词或助记码。例如:

rose rocket invest real refuse margin festival danger anger border idle brown

  • BIP44:基于 BIP32 的系统,赋予树状结构中的各层特殊的意义。让同一个 seed 可以支持多币种、多帐户等。各层定义如下:

m / purpose' / coin_type' / account' / change / address_index

其中的 purporse’ 固定是 44’,代表使用 BIP44。而 coin_type’ 用来表示不同币种,例如 Bitcoin 就是 0’,Ethereum 是 60’。

2 助记词生成

HPB Wallet目前使用的BIP39,将64位私钥变化为12个单词的形式便于记忆。

打开BIP39.swift文件,可以通过调用下面方法,随机生成一个助记词:

1
2
3
4
5
6
static public func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = BIP39Language.english) throws -> String? {
        guard bitsOfEntropy >= 128 && bitsOfEntropy <= 256 && bitsOfEntropy % 32 == 0 else {return nil}
        guard let entropy = Data.randomBytes(length: bitsOfEntropy/8) else {throw AbstractKeystoreError.noEntropyError}
        return BIP39.generateMnemonicsFromEntropy(entropy: entropy, language: language)
        
    }     

可以根据助记词去获取seed,然后通过sha256得到明文私钥

1
2
 let seed = BIP39.seedFromMmemonics(mnemonic, language: BIP39Language.english)
 let privateKey = seed.sha256()

2.1 助记词备份

拥有助记词就可以掌控这个账户,因此助记词生成后,要提醒用户去备份助记词。对于去中心化的APP,用户备份后要从本地删除,用户备份之前可以加密存放在本地。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB钱包基本概念

1 基本概念

1.1 什么是账户?

一个钱包地址就代表着一个账户。地址是账户的标识。地址表示的是该账户公钥的后20字节(通常会以0x开头,例如,0xed37f755e56b1d49642dce8ff2b788ae33263c94`)。每个账户都由一对钥匙定义,一个私钥(Private Key)和一个公钥(Public Key)。 账户以地址为索引,地址由公钥衍生而来,取公钥的最后20个字节。

1.2 私钥、公钥和地址

  1. 先生成一个私钥,由随机的256bit组成。
  2. 使用加密算法椭圆曲线签名算法elliptic curve cryptography将私钥映射生成公钥。一个私钥只能映射出一个公钥。
  3. 用公钥低位的160bit通过SHA-3加密hash算法计算得到地址。

1.3 Keystore文件

Keystore文件是JSON格式的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{  
   "address":"0xed37f755e56b1d49642dce8ff2b788ae33263c94",
   "crypto":{  
      "cipher":"aes-128-ctr",
      "ciphertext":"41c14f88ec8f35c9fe57cd39121a76c2dadbd82ea8fec59866468bc0d7371f2e",
      "cipherparams":{  
         "iv":"43443bf394e8f6ebcc687e13bc0effb9"
      },
      "kdf":"scrypt",
      "kdfparams":{  
         "dklen":32,
         "n":262144,
         "p":1,
         "r":8,
         "salt":"aaef6847d09cb1e9f5ceadaf5865d96a7493df1cae146b24e31092cc0a7844af"
      },
      "mac":"5e9781c587db5795c6d41cb4f001bf086cc3db33b6e7eefcc2ef472145e76821"
   },
   "id":"bcd61a88-283f-4d81-8457-30ec9c11521f",
   "version":3
}

通过keystore文件中的内容,我们可以看到其中包括了私钥加密的相关信息:

  • address:该账户的地址
  • cipher:加密方法使用的是AES-128-CTR算法4
  • ciphertext:加密后的密文
  • cipherparams:AES-128-CTR算法加密所需的相关参数
  • kdf:秘钥生成函数,用于使用密码对keystore文件进行加密
  • kdfparams:kdf算法所需的参数
  • mac:用于验证密码的编码

2 创建代码

2.1 创建EthereumKeystoreV3对象

在web3siwft中找到EthereumKeystoreV3.swift文件,创建EthereumKeystoreV3对象。构造函数生成对象:

1
2
3
4
5
6
//随机生成私钥
public init? (password: String = "BANKEXFOUNDATION") 
 
//指定私钥   
public init? (privateKey: Data, password: String = "BANKEXFOUNDATION") 
   

在构造方法中会调用encryptDataToStorage方法通过ECC去给keystoreParams赋值。

2.2 创建kstore文件并存入本地

对EthereumKeystoreV3的keystoreParams属性进行编码并存在本地。

具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static func generateKstoreFileBy(_ privateKey: Data,password: String,complete: ((String,String)->Void)?) 
   -> WalletManagerResult{
    
    guard let ethereumKeystore = try? EthereumKeystoreV3(privateKey: privateKey, password: password),let ks = ethereumKeystore else{
        return WalletManagerResult(false,"生成Keystore文件出错")
    }
    guard  let keydata = try? JSONEncoder().encode(ks.keystoreParams)
        else{
        return WalletManagerResult(false,"获取Keystore文件出错")
    }
    guard let adress = ks.getAddress() else{
       return WalletManagerResult(false,"获取Keystore文件出错")
    }
    //创建普通的keystore文件
    let filename = HPBFileManager.generateFileName(address: adress.addressData)
    if FileManager.default.createFile(atPath: HPBFileManager.getKstoreDirectory() + filename, contents: keydata, attributes: nil){
        complete?(filename,adress.address.lowercased())
        return WalletManagerResult(true,nil)
    }else{
        return  WalletManagerResult(false,"生成Keystore文件出错")
    }
}

到此,一个新的账户就创建成功了,keystore文件就是加密的账户私钥,需要配合密码使用。

下一节我们会讲解助记词生成和备份。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB钱包Xcode集成

1 集成说明

由于HPB主网账户底层算法和以太坊相同,因此我们可以采用以太坊的底层算法对接HPB。熟悉以太坊的朋友都知道,以太坊提供了一个Web3.js API 中文文档的库,通过调用js的代码去实现区块链DApp的开发。

但是对于iOS的小伙伴来说,直接调用js无疑是很困难的,并且从实现上来说也很复杂。因此我们找到了一个swift代码实现的web3swift的库,通过这个我们可以使用swift语言去开发应用,这让实现变得简单。接下来,我们就以这个库为基础,详细介绍钱包的开发过程。

2 Xcode工程设置

2.1 开发环境

  • 支持iOS 9.0以上系统
  • 使用Xcode 9或更高版本
  • swift4.1开发语言

2.2 Xcode工程设置

库文件的导入使用cocoapods,详细安装步骤,请点击

1. 创建Podfile
1
touch Podfile

创建Podfile

2. 使用CocoaPods 安装 web3swift

目前HPB Wallet采用 web3swift 0.7.0版本,后续也会以该版本作为基础进行说明。

1
2
3
4
5
6
platform :ios, '9.0'

target '<Your Target Name>' do
    use_frameworks!
    pod 'web3swift', '~> 0.7.0'
end

pod

在Podfile所在的文件夹下输入命令:

1
pod install 
3. 导入成功,启动工程

在使用的地方导入头文件。

导入

2.3使用CocoaPods的问题

  • pod search无法搜索到类库的解决办法(找不到类库)

(1)执行pod setup

(2)删除~/Library/Caches/CocoaPods目录下的search_index.json文件

1
2
3
pod setup成功后会生成~/Library/Caches/CocoaPods/search_index.json文件。
终端输入rm ~/Library/Caches/CocoaPods/search_index.json
删除成功后再执行pod search

(3)执行pod search

安装好了web3swift库,接下来我们会讲解如何创建钱包。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB 钱包简介

1 去中心化钱包基本概念

1.1 简介

我们生活中常用的钱包(支付宝,微信,银行卡等)是由中心化的机构发放,并进行资产管理,我们密码丢失,可以通过相关证明去找回密码。同样的,区块链交易所(bibBox,火币,OKEx等)也是中心化的,由交易所保存我们的私钥。使用交易所非常方便快捷,但其实不建议把大量的数字资产长期保存在交易所中,因为中心化交易所拥有大量的数字代币,容易成为黑客的重点攻击对象。因此我们就需要一款去中心化的区块链钱包保存我们的资产。HPB Wallet 就是一款去中心化的钱包。

1.2 钱包名词

区块链钱包一般包含这几个名词:公钥、私钥、助记词、keystore、密码。

1.2.1 地址

生成区块链钱包开发后会生成一个以 0x 开头的 42 位字符串,这个字符串就是区块链钱包地址。

1.2.2 私钥

这个私钥属于明文私钥,由 64 位字符串组成,一个钱包只有一个私钥且不能修改。谁拥有私钥(不需要原密码)就拥有这个钱包的掌控权。因此,创建完钱包要及时备份私钥,并妥善保存。

1.2.3 助记词

私钥一般太难记忆了,使用也不方便,所以从钱包设计的角度,就为简化操作同时不丢失安全性,就出现了助记词的方法。助记词是明文私钥的另一种表现形式,具有和私钥同样的功能,在导入区块链钱包中,输入助记词并设置一个密码(不用输入原密码)就拥有这个钱包的掌控权,因此,助记词和明文私钥同等重要。

1.2.4 keystore

keystore 属于加密私钥,和区块链钱包密码有很大关联,钱包密码修改后,keystore 也就相应变化,在用 keystore 导入钱包时,需要输入原密码。

1.3 优缺点

1.3.1 交易所的弊端

数字资产交易所未来是否会引入监管还存在极大的不确定性。最重要的一点是人们在交易所上存放的数字资产本质上并不掌握所有权,人们只是把数字资产转到了交易所指定的账号上,由交易所代为管理。所以交易所完全有可能直接用这些数字资产去做短期套利。交易所由于缺乏监管,理论上完全有可能携款跑路,并且极易受黑客攻击。

1.3.2 去中心化钱包

前面讲到了,区块链交易所越来越多的安全事件,让大家意识到使用去中心化钱包的重要性。所谓去中心化钱包,就是创建钱包的过程在本地生成,钱包服务商不保存我们的私钥,所以,相比中心化交易所不会出现交易所被攻击,自己资产丢失的情况。钱包安全程度与你私钥保管有很大的关系。所以,这里要提醒大家,如果使用去中心化的钱包,千万不要将私钥在手机上截图,或通过邮箱、QQ等方式传送,也不要放在云盘等云服务器,建议断网保存。大家可以在一张纸上写下私钥,然后妥善保管。

2 HPB Wallet钱包介绍

HPB Wallet 就是一款去中心化的钱包,可用于存储HPB资产。目前,HPB Wallet提供了用户创建,导入账号,存储HPB资产,转账,交易记录查询,映射,投票等功能。

2.1 钱包核心功能介绍

HPB Wallet 可以在首页查看当前的HPB资产,以及收款和转账功能

转账交易

HPB Wallet 提供用户管理钱包的功能,用户可以创建多个钱包,也可以导入钱包。 钱包管理

HPB Wallet 提供便捷的方式,让以太坊的HPB搬家,并且可以参与投票选出心目中的高性能节点 未命名_meitu_3.jpg

2.2 HPB Wallet的优势

HPB Wallet 基于HPB高性能主链,转账近乎秒级确认,这在业内是很具挑战性的突破。HPB Wallet 让交易更加便捷。

基本概念掌握清楚了,那么接下来的文章我们会讲解如何在HPB主网上开发钱包。

作者:感谢HPB Wallet 开发团队整理供稿

HPB58:以太坊数据结构与存储分析

一.概述

在以太坊中,数据的存储大致分为三个部分,分别是:状态数据、区块链和底层数据。

其中,底层数据存放以太坊中全部数据,存储形式是[k,v]键值对,目前使用数据库是LevelDB;所有与交易,操作相关的数据,都存储在链上;StateDB 是用来管理账户的,每个账户都是一个 stateObject。

二.区块部分

区块结构:

区块链是以太坊的核心之一,所有交易以及结构都存于一个个区块中,接下来我们看看以太坊中的区块结构是怎样的。

以太坊中所有结构定义基本都可以在 core/types 中找到,区块结构定义在 block.go

中:

受比特币区块链数据结构的影响,我以为 block 可以简单地分为 head 和 body 两部分。但读了以太坊源码后,我发现 Ethereum 不是这样设计的。在 block.go 中,定义了以太坊区块链的区块结构为:

可以看到 body+head!=block。除此之外,block.go 文件中还定义了许多结构,比如用于

协议的 extblock,用于存储的 storageblock 等。

可以看到,header 结构用的非常频繁,接下来看看 header 结构是如何定义的:

Header 成员变量的解释如下:

  • ParentHash:指向父区块(parentBlock)的指针。除了创世块(Genesis Block)外,每个区块有且只有一个父区块。
  • Coinbase:挖掘出这个区块的矿工地址。用于发放奖励。
  • UncleHash:Block 结构体的成员 uncles 的哈希值。注意这个字段是一个 Header 数
  • 组。
  • Root:StateDB 中的“state Trie”的根节点的哈希值。Block 中,每个账户以 stateObject 对象表示,账户以 Address 为唯一标示,其信息在相关交易(Transaction)的执行中被修改。所有账户对象可以逐个插入一个 Merkle-PatricaTrie(MPT)结构里,形成“state
  • Trie”。
  • TxHash: Block 中 “tx Trie”的根节点的哈希值。Block 的成员变量 transactions 中所有的
  • tx 对象,被逐个插入一个 MPT 结构,形成“tx Trie”。
  • ReceiptHash:Block 中的 “Receipt Trie”的根节点的哈希值。Block 的所有 Transaction 执行完后会生成一个 Receipt 数组,这个数组中的所有 Receipt 被逐个插入一个 MPT 结构中,形成”Receipt Trie”。(比特币区块中有一颗用于存放交易的梅克尔树,而以太坊中有不同功能的三棵 M-P 树)
  • Bloom:Bloom 过滤器(Filter),用来快速判断一个参数 Log 对象是否存在于一组已知的 Log 集合中。
  • Difficulty:区块的难度。Block 的 Difficulty 由共识算法基于 parentBlock 的 Time 和Difficulty 计算得出,它会应用在区块的‘挖掘’阶段。
  • Number:区块的序号。Block 的 Number 等于其父区块 Number +1。
  • Time:区块“应该”被创建的时间。由共识算法确定,一般来说,要么等于parentBlock.Time + 10s,要么等于当前系统时间。
  • GasLimit:区块内所有 Gas 消耗的理论上限。该数值在区块创建时设置,与父区块有关。具体来说,根据父区块的 GasUsed 同 GasLimit * 2/3 的大小关系来计算得出。
  • GasUsed:区块内所有 Transaction 执行时所实际消耗的 Gas 总和。
  • Nonce:一个 64bit 的哈希数,用于“挖矿”。

区块存储:

区块的数据最终都是以[k,v]的形式存储在数据库中,数据库在主机中的存储位置为:

datadir/geth/chaindata。具体代码在 core/database_util.go 中。区块在储存是,head 和

body 是分开存储的。

以 writeHeader 为例:

在存储 header 时,内容部分的 key 为:前缀+num(uint64 big endian)+hash;value 是区块

头的 rlp 编码。其余的模块存储方式类似:

各种前缀也都定义在 database_util.go 文件中,值得注意的是,在此文件中还有用于只存储区块头的一套函数,应该是为提高以太坊的灵活性。

三.交易部分

交易部分的内容比较多,已写在专门的一个文档,请查看上一篇内容。

四.Merkle-PatriciaTrie

Merkle-PatriciaTrie 简介:

Ethereum 使用的 Merkle-PatriciaTrie(MPT)结构,源自于 Trie 结构,又分别继承了

PatriciaTrie 和 MerkleTree 的优点,并基于内部数据的特性,设计了全新的节点体系和插入载入机制。

Trie,又称为字典树或者前缀树(prefix tree),属于查找树的一种。它与平衡二叉树的主要不同点包括:每个节点数据所携带的 key 不会存储在 Trie 的节点中,而是通过该节点在整个树形结构里位置来体现;同一个父节点的子节点,共享该父节点的 key 作为它们各自 key 的前缀,因此根节点 key 为空;待存储的数据只存于叶子节点中,非叶子节点帮助形成叶子节点 key 的前缀。下图来自 wiki-Trie,展示了一个简单的 Trie 结构。

PatriciaTrie,又被称为 RadixTree 或紧凑前缀树(compact prefix tree),是一种空间使用率经过优化的 Trie。与 Trie 不同的是,PatriciaTrie 里如果存在一个父节点只有一个子节点, 那么这个父节点将与其子节点合并。这样可以缩短 Trie 中不必要的深度,大大加快搜索节点速度。

MerkleTree,也叫哈希树(hash tree),是密码学的一个概念,注意理论上它不一定是 Trie。在哈希树中,叶子节点的标签是它所关联数据块的哈希值,而非叶子节点的标签是它的所有子节点的标签拼接而成字符串的哈希值。哈希树的优势在于,它能够对大量的数据内容迅速作出高效且安全的验证。假设一个 hash tree 中有 n 个叶子节点,如果想要验证其中一个叶子节点是否正确-即该节点数据属于源数据集合并且数据本身完整,所需哈希计算的时间复杂度是是 O(log(n)),相比之下 hash list 大约需要时间复杂度 O(n)的哈希计算,hash

tree 的表现无疑是优秀的。

上图来自 wiki-MerkleTree,展示了一个简单的二叉哈希树。四个有效数据块 L1-L4, 分别被关联到一个叶子节点上。Hash0-0 和 Hash0-1 分别等于数据块 L1 和 L2 的哈希值, 而 Hash0 则等于 Hash0-0 和 Hash0-1 二者拼接成的新字符串的哈希值,依次类推,根节点的标签 topHash 等于 Hash0 和 Hash1 二者拼接成的新字符串的哈希值。比特币在存储交易时就用的此种数据结构。

实现部分

MPT 的相关代码在 tire 文件夹中。

Node.go 中定义了四种类型的节点,分别是:

其中,nodeFlag 是用于在创建/修改节点是存放缓存数据的。

fullNode 是一个可以携带多个子节点的父(枝)节点。它有一个容量为 17 的 node 数组成员变量 Children,数组中前 16 个空位分别对应 16 进制(hex)下的 0-9a-f,这样对于每个子节点,根据其 key 值 16 进制形式下的第一位的值,就可挂载到 Children 数组的某个位置,fullNode 本身不再需要额外 key 变量;Children 数组的第 17 位,留给该 fullNode 的数据部分。fullNode 明显继承了原生 trie 的特点,而每个父节点最多拥有 16 个分支也包含了基于总体效率的考量。

shortNode 是一个仅有一个子节点的父(枝)节点。它的成员变量 Val 指向一个子节点,而成员 Key 是一个任意长度的字符串(字节数组[]byte)。显然 shortNode 的设计体现了PatriciaTrie 的特点,通过合并只有一个子节点的父节点和其子节点来缩短 trie 的深度,结果就是有些节点会有长度更长的 key。

valueNode 充当 MPT 的叶子节点。它其实是字节数组[]byte 的一个别名,不带子节点。在使用中,valueNode 就是所携带数据部分的 RLP 哈希值,长度 32byte,数据的 RLP 编码值作为 valueNode 的匹配项存储在数据库里。

这三种类型覆盖了一个普通 Trie(也许是 PatriciaTrie)的所有节点需求。任何一个[k,v]类型数据被插入一个 MPT 时,会以 k 字符串为路径沿着 root 向下延伸,在此次插入结束时首先成为一个 valueNode,k 会以自顶点 root 起到到该节点止的 key path 形式存在。但之后随着其他节点的不断插入和删除,根据 MPT 结构的要求,原有节点可能会变化成其他node 实现类型,同时 MPT 中也会不断裂变或者合并出新的(父)节点。比如:

假设一个 shortNode S 已经有一个子节点 A,现在要新插入一个子节点 B,那么会有两种可能,要么新节点 B 沿着 A 的路径继续向下,这样 S 的子节点会被更新;要么 S 的Key 分裂成两段,前一段分配给 S 作为新的 Key,同时裂变出一个新的 fullNode 作为 S 的子节点,以同时容纳 B,以及需要更新的 A。

如果一个 fullNode 原本只有两个子节点,现在要删除其中一个子节点,那么这个fullNode 就会退化为 shortNode,同时保留的子节点如果是 shortNode,还可以跟它再合并。

如果一个 shortNode 的子节点是叶子节点同时又被删除了,那么这个 shortNode 就会退化成一个 valueNode,成为一个叶子节点。诸如此类的情形还有很多,提前设想过这些案例,才能正确实现 MPT 的插入/删除/查找等操作。当然,所有查找树(search tree)结构的操作,免不了用到递归。

hashNode 跟 valueNode 一样,也是字符数组[]byte 的一个别名,同样存放 32byte 的哈希值,也没有子节点。不同的是,hashNode 是 fullNode 或者 shortNode 对象的 RLP 哈希值,所以它跟 valueNode 在使用上有着莫大的不同。

在 MPT 中,hashNode 几乎不会单独存在(有时遍历遇到一个 hashNode 往往因为原本的 node 被折叠了),而是以 nodeFlag 结构体的成员(nodeFlag.hash)的形式,被 fullNode 和shortNode 间接持有。一旦 fullNode 或 shortNode 的成员变量(包括子结构)发生任何变化,它们的 hashNode 就一定需要更新。所以在 trie.Trie 结构体的 insert(),delete()等函数实现中,可以看到除了新创建的 fullNode、shortNode,那些子结构有所改变的 fullNode、shortNode 的 nodeFlag 成员也会被重设,hashNode 会被清空。在下次 trie.Hash()调用时,整个 MPT 自底向上的遍历过程中,所有清空的 hashNode 会被重新赋值。这样trie.Hash()结束后,我们可以得到一个根节点 root 的 hashNode,它就是此时此刻这个 MPT结构的哈希值。

Header 中的成员变量 Root、TxHash、ReceiptHash 的生成,正是源于此。明显的,hashNode 体现了 MerkleTree 的特点:每个父节点的哈希值来源于所有子节点哈希值的组合,一个顶点的哈希值能够代表一整个树形结构。hashNode 加上之前的fullNode,shortNode,valueNode,构成了一个完整的 Merkle-PatriciaTrie 结构,很好的融合了各种原型结构的优点,非常值得研究。

节点增删查改用到的函数都定义于 trie.go。在 MPT 的查找,插入,删除过程中,如果在遍历时遇到一个 hashNode,首先需要从数据库里以这个哈希值为 k,读取出相匹配的v,然后再将 v 解码恢复成 fullNode 或 shortNode。在代码中这个过程叫 resolve。

五.StateDB

在系统设计中,底层数据库模块和业务模型之间,往往需要设置本地存储模块,它面向业务模型,可以根据业务需求灵活的设计各种存储格式和单元,同时又连接底层数据库,如果底层数据库(或者第三方 API)有变动,可以大大减少对业务模块的影响。在以太坊中,StateDB 就担任这个角色,它通过大量的 stateObject 对象集合,管理所有“账户”信息。

StateDB 有一个 trie.Trie 类型成员 trie,它又被称为 storage trie 或 stte trie,这个 MPT 结构中存储的都是 stateObject 对象,每个 stateObject 对象以其地址(20 bytes)作为插入节点的 Key;每次在一个区块的交易开始执行前,trie 由一个哈希值(hashNode)恢复出来。另外还有一个 map 结构,也是存放 stateObject,每个 stateObject 的地址作为 map 的 key。那么问题来了,这些数据结构之间是怎样的关系呢?

如上图所示,每当一个 stateObject 有改动,亦即“账户”信息有变动时,这个stateObject 对象会更新,并且这个 stateObject 会标为 dirty,此时所有的数据改动还仅仅存储在 map 里。当 IntermediateRoot()调用时,所有标为 dirty 的 stateObject 才会被一起写入 trie。而整个 trie 中的内容只有在 CommitTo()调用时被一起提交到底层数据库。可见,这个 map 被用作本地的一级缓存,trie 是二级缓存,底层数据库是第三级,各级数据结构的界限非常清晰,这样逐级缓存数据,每一级数据向上一级提交的时机也根据业务需求做了合理的选择。

StateDB 还可以管理账户状态的版本。这个功能用到了几个结构体:journal,revision,先来看看 UML 关系图:

其中 journal 对象是 journalEntry 的散列,长度不固定,可任意添加元素,接口journalEntry 存在若干种实现体,描述了从单个账户操作(账户余额,发起合约次数等),到account trie 变化(创建新账户对象,账户消亡)等各种最小事件。revision 结构体,用来描述一个‘版本’,它的两个整型成员 jd 和 journalIndex,都是基于 journal 散列进行操作的。

上图简述了 StateDB 中账户状态的版本是如何管理的。首先 journal 散列会随着系统运行不断的增长,记录所有发生过的单位事件;当某个时刻需要产生一个账户状态版本时, 代码中相应的是 Snapshop()调用,会产生一个新 revision 对象,记录下当前 journal 散列的长度,和一个自增 1 的版本号。

基于以上的设计,当发生回退要求时,只要根据相应的 revision 中的 journalIndex,在journal 散列上,根据所记录的所有 journalEntry,即可使所有账户回退到那个状态。每个 stateObject 对象管理着以太坊中的一个“账户”。stateObject 有一个成员变量data,类型是 Accunt 结构体,里面存有账户 Ether 余额,合约发起次数,最新发起合约指令集的哈希值,以及一个 MPT 结构的顶点哈希值。

stateObject 内部也有一个 Trie 类型的成员 trie,被称为 storage trie,它里面存放的是一种被称为 State 的数据。State 跟每个账户相关,格式是[Hash, Hash]键值对。有意思的是,stateObject 内部也有类似 StateDB 一样的二级数据缓存机制,用来缓存和更新这些State。

stateObject 定义了一种类型名为 storage 的 map 结构,用来存放[Hash,Hash]类型的数据对,也就是 State 数据。当 SetState()调用发生时,storage 内部 State 数据被更新,相应标示为”dirty”。之后,待有需要时(比如 updateRoot()调用),那些标为”dirty”的 State 数据被一起写入 storage trie,而 storage trie 中的所有内容在 CommitTo()调用时再一起提交到底层数据库。

感谢HPB团队整理。

HPB57:ETH交易部分分析

1.交易结构

交易结构定义在 core/types/transaction.go 中:

这个 atomic 是 go 语言的一个包 sync/atomic,用来实现原子操作。在这个结构体中, data 为数据字段,其余三个为缓存。下面是计算 hash 的函数:

计算哈希前,首先会从缓存 tx.hash 中获取,如果取到,则直接返回值。没有,则使用

rlpHash 计算:

hash 的计算方式为:先将交易的 tx.data 进行 rlpEncode 编码(定义在:core/types/transaction.go 中)

然后再进行算法为 Keccak256 的哈希计算。即:txhash=Keccak256(rlpEncode(tx.data))

Transaction 中,data 为 txdata 类型的,定义于同文件中,里面详细规定了交易的具体字段:

这些字段的详细解释如下:

  • AccountNonce:此交易的发送者已发送过的交易数(可防止重放攻击)
  • Price:此交易的 gas price
  • GasLimit:本交易允许消耗的最大 gas 数量
  • Recipient:交易的接收者地址,如果这个字段为 nil 的话,则这个交易为“合约创建”类型交易
  • Amount:交易转移的以太币数量,单位是 wei
  • Payload:交易可以携带的数据,在不同类型的交易中有不同的含义
  • V R S:交易的签名数据

我们会发现,交易中没有包含发送者地址这条数据,这是因为这个地址已包含在签名信息中,后面我们会分析到相关代码,另外,以太坊节点还会提供 JSON RPC 服务,供外部调用来传输数据。传输的数据格式为 json,因此,本文件中,还定义了交易的 json 类型数据结构,以及相关的转换函数。

函数为:MarshalJSON()和 UnmarshlJSON(),这两个函数会调用core/types/gen_tx_json.go 文件中的同名函数进行内外部数据类型的转换。

2.交易存储

交易的获取与存储函数为:Get/WriteTXLookupEntries ,定义在 core/database_util.go

中。

对于每个传入的区块,该函数会读取块中的每一条交易来分别处理。首先建立条目(entry),数据类型为:txLookupEntry。内容包括区块哈希、区块号以及交易索引(交易 在区块中的位置),然后将此 entry 进行 rlp 编码作为存入数据库的 value。key 部分与区块存储类似,组成结构为交易前缀+交易哈希。

此函数的调用主要在 core/blockchain.go 中,比如 WriteBlockAndState()会将区块写入数据库,处理 body 部分时需要分别处理每条交易。而 WriteBlockAndState 是在miner/worker.go 中 wait 函数调用的。mainer/worker.go 中 newWorker 函数在创建新矿工时,会调用 worker.wait().

3.交易类型

在源码中交易只有一种数据结构,如果非要给交易分个类的话,我认为交易可以分为三种:转账的交易、创建合约的交易、执行合约的交易。web3.js 提供了发送交易的接口:

web3.eth.sendTransaction(transactionObject [, callback]) (web3.js 在internal/jsre/deps 中)

参数是一个对象,如果在发送交易的时候指定不同的字段,区块链节点就可以识别出对应类型的交易。

转账交易

​ 转账是最简单的一种交易,这里转账是指从一个账户向另一个账户发送以太币。发送转账交易的时候只需要指定交易的发送者、接收者、转币的数量。使用 web3.js 发送转账交易应该像这样:

value 是转移的以太币数量,单位是 wei,对应的是源码中的 Amount 字段。to 对应的是源码中的 Recipient

创建合约交易

​ 创建合约指的是将合约部署到区块链上,这也是通过发送交易来实现。在创建合约的交易中,to 字段要留空不填,在 data 字段中指定合约的二进制代码,from 字段是交易的发送者也是合约的创建者。

data 字段对应的是源码中的 Payload 字段。

执行合约交易

调用合约中的方法,需要将交易的 to 字段指定为要调用的合约的地址,通过 data 字段指定要调用的方法以及向该方法传递的参数。

data 字段需要特殊的编码规则,具体细节可以参考 Ethereum Contract ABI(自己拼接字段既不方便又容易出错,所以一般都使用封装好的 SDK(比如 web3.js) 来调用合约)。

4.交易执行

​ 按照以太坊架构设计,交易的执行可大致分为内外两层结构:第一层是虚拟机外,包括执行前将 Transaction 类型转化成 Message,创建虚拟机(EVM)对象,计算一些 Gas 消耗,以及执行交易完毕后创建收据(Receipt)对象并返回等;第二层是虚拟机内,包括执行 转帐,和创建合约并执行合约的指令数组。

虚拟机外

执行 tx 的入口函数是 Process()函数,在 core/state_processor.go 中。

​ Process()函数的核心是一个 for 循环,它将 Block 里的所有 tx 逐个遍历执行。具体的执行函数为同个 go 文件中的 ApplyTransaction()函数,它每次执行 tx, 会返回一个收据(Receipt)对象。Receipt 结构体的声明如下(core/types/receipt.go):

​ Receipt 中有一个 Log 类型的数组,其中每一个 Log 对象记录了 Tx 中一小步的操作。所以,每一个 tx 的执行结果,由一个 Receipt 对象来表示;更详细的内容,由一组 Log 对象来记录。这个 Log 数组很重要,比如在不同 Ethereum 节点(Node)的相互同步过程中, 待同步区块的 Log 数组有助于验证同步中收到的 block 是否正确和完整,所以会被单独同步(传输)。

Receipt 的 PostState 保存了创建该 Receipt 对象时,整个 Block 内所有“帐户”的当时状态。Ethereum 里用 stateObject 来表示一个账户 Account,这个账户可转帐(transfer value), 可执行 tx, 它的唯一标示符是一个 Address 类型变量。 这个 Receipt.PostState 就是当时所在 Block 里所有 stateObject 对象的 RLP Hash 值。

Bloom 类型是一个 Ethereum 内部实现的一个 256bit 长 Bloom Filter。 Bloom Filter 概念定义可见 wikipediahttp://blog.csdn.net/jiaomeng/article/details/1495500 它可用来快速验证一个新收到的对象是否处于一个已知的大量对象集合之中。这里 Receipt 的 Bloom, 被用以验证某个给定的 Log 是否处于 Receipt 已有的 Log 数组中。

​ 我们来看下 StateProcessor.ApplyTransaction()的具体实现,它的基本流程如下图:

​ ApplyTransaction()首先根据输入参数分别封装出一个 Message 对象和一个 EVM 对象,然后加上一个传入的 GasPool 类型变量,执行 core/state_transition.go 中的ApplyMessage(),而这个函数又调用同 go 文件中 TransitionDb()函数完成 tx 的执行,待TransitionDb()返回之后,创建一个收据 Receipt 对象,最后返回该 Recetip 对象,以及整个tx 执行过程所消耗 Gas 数量。

GasPool 对象是在一个 Block 执行开始时创建,并在该 Block 内所有 tx 的执行过程中共享,对于一个 tx 的执行可视为“全局”存储对象; Message 由此次待执行的 tx 对象转化而来,并携带了解析出的 tx 的(转帐)转出方地址,属于待处理的数据对象;EVM 作为Ethereum 世界里的虚拟机(Virtual Machine),作为此次 tx 的实际执行者,完成转帐和合约(Contract)的相关操作。

我们来细看下 TransitioinDb()的执行过程(/core/state_transition.go)。假设有StateTransition 对象 st, 其成员变量 initialGas 表示初始可用 Gas 数量,gas 表示即时可用Gas 数量,初始值均为 0,于是 st.TransitionDb() 可由以下步骤展开:

首先执行 preCheck()函数,检查:1.交易中的 nonce 和账户 nonce 是否为同一个。2. 检查 gas 值是否合适(<=64 )

  • 购买 Gas。首先从交易的(转帐)转出方账户扣除一笔 Ether,费用等于tx.data.GasLimit * tx.data.Price; 同 时 st.initialGas = st.gas = tx.data.GasLimit; 然 后(GasPool) gp –= st.gas 。
  • 计算 tx 的固有 Gas 消耗 – intrinsicGas。它分为两个部分,每一个 tx 预设的消耗量,这个消耗量还因 tx 是否含有(转帐)转入方地址而略有不同;以及针对tx.data.Payload 的 Gas 消耗,Payload 类型是[]byte,关于它的固有消耗依赖于[]byte 中非 0 字节和 0 字节的长度。最终,st.gas –= intrinsicGas
  • EVM 执行。如果交易的(转帐)转入方地址(tx.data.Recipient)为空,即contractCreation,调用 EVM 的 Create()函数;否则,调用 Call()函数。无论哪个函数返回后,更新 st.gas。
  • 计算本次执行交易的实际 Gas 消耗: requiredGas = st.initialGas – st.gas
  • 偿退 Gas。它包括两个部分:首先将剩余 st.gas 折算成 Ether,归还给交易的(转帐)转出方账户;然后,基于实际消耗量 requiredGas,系统提供一定的补偿,数量为 refundGas。refundGas 所折算的 Ether 会被立即加在(转帐)转出方账户上, 同时 st.gas += refundGas,gp += st.gas,即剩余的 Gas 加上系统补偿的 Gas,被一起归并进 GasPool,供之后的交易执行使用。
  • 奖励所属区块的挖掘者:系统给所属区块的作者,亦即挖掘者账户,增加一笔金额,数额等于 st.data,Price * (st.initialGas – st.gas)。注意,这里的 st.gas 在步骤 5 中被加上了 refundGas, 所以这笔奖励金所对应的 Gas,其数量小于该交易实际消耗量 requiredGas。

由上可见,除了步骤 3 中 EVM 函数的执行,其他每个步骤都在围绕着 Gas 消耗量作文章。

步骤 5 的偿退机制很有意思,设立它的目的何在?目前为止我只能理解它可以避免交易执行过程中过快消耗 Gas,至于对其全面准确的理解尚需时日。

步骤 6 是奖励机制,没什么好说的。

Ethereum 中每个交易(transaction,tx)对象在被放进 block 时,都是经过数字签名的, 这样可以在后续传输和处理中随时验证 tx 是否经过篡改。Ethereum 采用的数字签名是椭圆曲线数字签名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比于基于大质数分解的 RSA 数字签名算法,可以在提供相同安全级别(in bits)的同时,仅需更短的公钥(public key)。这里需要特别留意的是,tx 的转帐转出方(发送方)地址,就是对该 tx 对象作 ECDSA 签名计算时所用的公钥 publicKey。

Ethereum 中的数字签名计算过程所生成的签名(signature), 是一个长度为 65bytes 的字节数组,它被截成三段放进 tx 中,前 32bytes 赋值给成员变量 R, 再 32bytes 赋值给 S,1byte 赋给 V,当然由于 R、S、V 声明的类型都是*big.Int, 上述赋值存在[]byte –> big.Int 的类型转换。

当需要恢复出 tx 对象的转帐转出方地址时(比如在需要执行该交易时),Ethereum 会先从 tx 的 signature 中恢复出公钥,再将公钥转化成一个 common.Address 类型的地址,signature 由 tx 对象的三个成员变量 R,S,V 转化成字节数组[]byte 后拼接得到。

Ethereum 对此定义了一个接口 Signer, 用来执行挂载签名,恢复公钥,对 tx 对象做哈希等操作。 接口定义是在:/ core/types/transaction_signing.go 的:

这个接口主要做的就是恢复发送地址、生成签名格式、生成交易哈希、验证等。

生成数字签名的函数叫 SignTx(),最根源是定义在 core/types/transaction_signing.go(mobile/accounts.go 中也有 SignTx,但是这个函数是调用 accounts/keystore/keystore.go中的 SignTX,最终又调用 types.SignTx),它会先调用其函数生成 signature, 然后调用tx.WithSignature()将 signature 分段赋值给 tx 的成员变量 R,S,V。

​ Signer 接口中,恢复(提取?)转出方地址的函数为:Sender,Sender returns the address derived from the signature (V, R, S) using secp256k1。使用到的参数是:Signer 和 Transaction ,该函数定义在core/types/transaction_signing.go 中

​ Sender()函数体中,signer.Sender()会从本次数字签名的签名字符串(signature)中恢复出公钥,并转化为 tx 的(转帐)转出方地址。此函数最终会调用同文件下的 recoverPlain 函数来进行恢复

在上文提到的 ApplyTransaction()实现中,Transaction 对象需要首先被转化成 Message接口,用到的AsMessage()函数即调用了此处的 Sender()。调用路径为: AsMessage->transaction_signing.Sender(两个参数的)–>sender(单个参数的) 在 Transaction 对象 tx 的转帐转出方地址被解析出以后,tx 就被完全转换成了Message 类型,可以提供给虚拟机 EVM 执行了。

虚拟机内:

​ 每个交易(Transaction)带有两部分内容(参数)需要执行:

  1. 转帐,由转出方地址向转入方地址转帐一笔以太币 Ether;
  2. 携带的[]byte 类型成员变量 Payload,其每一个 byte 都对应了一个单独虚拟机指令。这些内容都是由 EVM(Ethereum Virtual Machine)对象来完成 的。EVM 结构体是 Ethereum 虚拟机机制的核心,它与协同类的 UML 关系图如下:

​ 其中 Context 结构体分别携带了 Transaction 的信息(GasPrice, GasLimit),Block 的信息(Number, Difficulty),以及转帐函数等,提供给 EVM;StateDB 接口是针对 state.StateDB 结构体设计的本地行为接口,可为 EVM 提供 statedb 的相关操作; Interpreter 结构体作为解释器,用来解释执行 EVM 中合约(Contract)的指令(Code)。

​ 注意,EVM 中定义的成员变量 Context 和 StateDB, 仅仅声明了变量名而无类型,而变量名同时又是其类型名,在 Golang 中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如 EVM 调用 Context 中的 Transfer()。

交易的转帐操作由 Context 对象中的 TransferFunc 类型函数来实现,类似的函数类型,还有 CanTransferFunc, 和 GetHashFunc。这三个类型的函数变量 CanTransfer, Transfer, GetHash,在 Context 初始化时从外部传入,目前使用的均是一个本地实现。可见目前的转帐函数 Transfer()的逻辑非常简单,转帐的转出账户减掉一笔以太币,转入账户加上一笔以太币。由于 EVM 调用的 Transfer()函数实现完全由 Context 提供,所以,假设如果基于 Ethereum 平台开发,需要设计一种全新的“转帐”模式,那么只需写一个新的 Transfer()函数实现,在 Context 初始化时赋值即可。

有朋友或许会问,这里 Transfer()函数中对转出和转入账户的操作会立即生效么?万一两步操作之间有错误发生怎么办?答案是不会立即生效。StateDB 并不是真正的数据库, 只是一行为类似数据库的结构体。它在内部以 Trie 的数据结构来管理各个基于地址的账 户,可以理解成一个 cache;当该账户的信息有变化时,变化先存储在 Trie 中。仅当整个Block 要被插入到 BlockChain 时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。

合约的创建和赋值:

合约(Contract)是 EVM 用来执行(虚拟机)指令的结构体。Contract 的结构定义于:core/vm/contract.go 中,在这些成员变量里,caller 是转帐转出方地址(账户),self 是转入方地址,不过它们的类型都用接口 ContractRef 来表示;Code 是指令数组,其中每一个 byte 都对应于一个预定义的虚拟机指令;CodeHash 是 Code 的 RLP 哈希值;Input 是数据数组,是指令所操作的数据集合;Args 是参数。

​ 有意思的是 self 这个变量,为什么转入方地址要被命名成 self 呢? Contract 实现了ContractRef 接口,返回的恰恰就是这个 self 地址。

func (c *Contract) Address() common.Address { return c.self.Address()

}

​ 所以当 Contract 对象作为一个 ContractRef 接口出现时,它返回的地址就是它的 self地址。那什么时候 Contract 会被类型转换成 ContractRef 呢?当 Contract A 调用另一个Contract B 时,A 就会作为 B 的 caller 成员变量出现。Contract 可以调用 Contract,这就为系统在业务上的潜在扩展,提供了空间。

创建一个 Contract 对象时,重点关注对 self 的初始化,以及对 Code, CodeAddr 和Input 的赋值。

另外,StateDB 提供方法 SetCode(),可以将指令数组 Code 存储在某个 stateObject 对象中; 方法 GetCode(),可以从某个 stateObject 对象中读取已有的指令数组 Code。

1
2
3
func (self *StateDB) SetCode(addr common.Address, code []byte) /func (self

*StateDB) GetCode(addr common.Address) code []byte

​ stateObject (core/state/state_object.go)是 Ethereum 里用来管理一个账户所有信息修改的结构体,它以一个 Address 类型变量为唯一标示符。StateDB 在内部用一个巨大的map 结构来管理这些 stateObject 对象。所有账户信息-包括 Ether 余额,指令数组 Code,该账户发起合约次数 nonce 等-它们发生的所有变化,会首先缓存到 StateDB 里的某个stateObject 里,然后在合适的时候,被 StateDB 一起提交到底层数据库。

​ EVM(core/vm/evm.go)中 目前有五个函数可以创建并执行 Contract,按照作用和调用方式,可以分成两类:

  • ​ Create(), Call(): 二者均在 StateProcessor 的 ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。
  • ​ CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐。

    考虑到与执行交易的相关性,这里着重探讨 Create()和 Call()。先来看 Call(),它用来处理(转帐)转入方地址不为空的情况:

Call()函数的逻辑可以简单分为以上 6 步。其中步骤(3)调用了转帐函数 Transfer(),转入账户 caller, 转出账户 addr;步骤(4)创建一个 Contract 对象,并初始化其成员变量 caller, self(addr), value 和 gas; 步骤(5)赋值 Contract 对象的 Code, CodeHash, CodeAddr 成员变量;步骤(6) 调用 run()函数执行该合约的指令,最后 Call()函数返回。相关代码可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) 

{

if evm.vmConfig.NoRecursion && evm.depth > 0 {//如果设置了“禁用 call”,并且depth 正确,直接返回

return nil, gas, nil

}

// Fail if we're trying to execute above the call depth limit

  if evm.depth > int(params.CallCreateDepth) {//如果 call 的栈深度超过了预设值, 报错

return nil, gas, ErrDepth

}

// Fail if we're trying to transfer more than the available balance

    if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {//检查发出账户是否有足够的钱(实际实现的函数定义在     core/evm.go/CanTransfer()中)但目前还不知道是怎么调用的

  return nil, gas, ErrInsufficientBalance

}

var (

to = AccountRef(addr)

snapshot  = evm.StateDB.Snapshot()

)

if !evm.StateDB.Exist(addr) {//建立账户

precompiles := PrecompiledContractsHomestead

if evm.ChainConfig().IsByzantium(evm.BlockNumber) { precompiles = PrecompiledContractsByzantium

}

if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {

return nil, gas, nil

}

evm.StateDB.CreateAccount(addr)

}

evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)//转移

// initialise a new contract and set the code that is to be used by the

// E The contract is a scoped environment for this execution context

// only.

contract := NewContract(caller, to, value, gas)//建立合约contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr),

evm.StateDB.GetCode(addr))

ret, err = run(evm, snapshot, contract, input)

// When an error was returned by the EVM or when setting the creation code

// above we revert to the snapshot and consume any gas remaining. Additionally

// when we're in homestead this also counts for code storage gas errors. if err != nil {

evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted {

contract.UseGas(contract.Gas)

}

}

return ret, contract.Gas, err

}

​ 因为此时(转帐)转入地址不为空,所以直接将入参 addr 初始化 Contract 对象的 self 地址,并可从 StateDB 中(其实是以 addr 标识的账户 stateObject 对象)读取出相关的 Code 和CodeHash 并赋值给 contract 的成员变量。注意,此时转入方地址参数 addr 同时亦被赋值予 contract.CodeAddr。

再来看看 EVM.Create(),它用来处理(转帐)转入方地址为空的情况。

与 Call()相比,Create()因为没有 Address 类型的入参 addr,其流程有几处明显不同:

  • ​ 步骤(3)中创建一个新地址 contractAddr,作为(转帐)转入方地址,亦作为

    Contract 的 self 地址;

  • ​ 步骤(6)由于 contracrAddr 刚刚新建,db 中尚无与该地址相关的 Code 信息, 所以会将类型为[]byte 的入参 code,赋值予 Contract 对象的 Code 成员;

  • ​ 步骤(8)将本次执行合约的返回结果,作为 contractAddr 所对应账户

    (stateObject 对象)的 Code 储存起来,以备下次调用。

​ 还有一点隐藏的比较深,Call()有一个入参 input 类型为[]byte,而 Create()有一个入参code 类型同样为[]byte,没有入参 input,它们之间有无关系?其实,它们来源都是Transaction 对象 tx 的成员变量 Payload!调用 EVM.Create()或 Call()的入口在StateTransition.TransitionDb()中,当 tx.Recipent 为空时,tx.data.Payload 被当作所创建Contract 的 Code;当 tx.Recipient 不为空时,tx.data.Payload 被当作 Contract 的 Input。

预编译合约

​ EVM 中执行合约(指令)的函数是 run(),在 core/vm/evm.go 中其实现代码如下: 可见如果待执行的 Contract 对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr 为匹配项-那么它可以直接运行;没有经过预编译的 Contract,才会由Interpreter 解释执行。这里的”预编译”,可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要 Code,仅需 Input 即可。

在代码实现中,预编译合约只需实现两个方法 Required()和 Run()即可,这两方法仅需一个入参 input。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/ core/vm/contracts.go

type PrecompiledContract interface { RequiredGas(input []byte) uint64 Run(input []byte) ([]byte, error)

}

func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contrat) (ret []byte, err error) {

gas := p.RequiredGas(input) if contract.UseGas(gas) {

return p.Run(input)

}

return nil, ErrOutOfGas

}

目前,Ethereuem 代码中已经加入了多个预编译合约,功能覆盖了包括椭圆曲线密钥恢复,SHA-3(256bits)哈希算法,RIPEMD-160 加密算法等等。相信基于自身业务的需求,二次开发者完全可以加入自己的预编译合约,大大加快合约的执行速度。

解释器执行合约的指令

解释器 Interpreter 用来执行(非预编译的)合约指令。它的结构体 UML 关系图如下所示:

​ Interpreter 结构体通过一个 Config 类型的成员变量,间接持有一个包括 256 个operation 对象在内的数组 JumpTable。operation 是做什么的呢?

每个 operation 对象正对 应 一 个 已 定 义 的 虚 拟 机 指 令 , 它 所 含 有 的 四 个 函 数 变 量 execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。每个指令长度1byte,Contract 对象的成员变量 Code 类型为[]byte,就是这些虚拟机指令的任意集合,operation 对象的函数操作,主要会用到 Stack,Memory, IntPool 这几个自定义的数据结构。

​ 这样一来,Interpreter 的 Run()函数就很好理解了,其核心流程就是逐个 byte 遍历入参 Contract 对象的 Code 变量,将其解释为一个已知的 operation,然后依次调用该operation 对象的四个函数,流程示意图如下:

operation 在操作过程中,会需要几个数据结构: Stack,实现了标准容器 -栈的行为;Memory,一个字节数组,可表示线性排列的任意数据;还有一个 intPool,提供对big.Int 数据的存储和读取。

已定义的 operation,种类很丰富,包括:

  • ​ 算术运算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP…;
  • ​ 逻辑运算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT…;
  • ​ 业务功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2…等等

    需要特别注意的是 LOGn 指令操作,它用来创建 n 个 Log 对象,这里 n 最大是 4。还记得 Log 在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个 Receipt 对象用来记录这个交易的执行结果。Receipt 携带一个 Log 数组,用来记录 tx 操作过程中的所有变动细节,而这些 Log,正是通过合适的 LOGn 指令-即合约指令数组(Contract.Code) 中的单个 byte,在其对应的 operation 里被创建出来的。每个新创建的 Log 对象被缓存在StateDB 中的相对应的 stateObject 里,待需要时从 StateDB 中读取。

感谢HPB团队整理。

HPB56:Windows平台下eclipse配置

安装 go 环境

根据自己的系统版本下载安装包,进行 go 的环境的安装。下载地址:

https://www.golangtc.com/download

环境变量配置

对 go 的环境变量进行配置,有些安装是默认配置,有些需要手动的配置环境变量。

Go 环境验证,在控制台中输入 go version, 如显示以下内容,则表示输入成功。

下载安装 Eclipse

安装环境:

  • ​ Java VM version 8 or later.
  • ​ Eclipse 4.6 (Neon) or later.
  • ​ CDT 9.0 or later (this will be installed or updated automatically as part of the steps below).

下载安装 eclipse,下载地址如下:

http://archive.eclipse.org/eclipse/downloads/drops4/R-4.6- 201606061100/#PlatformRuntime

安装配置 mingw64

MinGW,是 Minimalist GNUfor Windows 的缩写。它是一个可自由使用和自由发布的 Windows 特定头文件和使用 GNU 工具集导入库的集合,允许你在 GNU/Linux Windows 平台生成本地的 Windows 程序而不需要第三方 C 运行时(C Runtime)库。

下 载MingW

Mingw 的官网是 http://www.mingw.org/ ,也可以通过 http://mingw-w64.org/doku.php 下载, 根据你的系统选择相应版本。

安装配置

双击进入系统配置页面

根据机器选择相应型号。

配置成功后的页面如下:

配置环境变量,首先配置 C_INCLUDE_PATH

C:\Program Files\mingw-w64\x86_64-7.2-win32-seh-rt_v5-rev0\mingw64\include

配置 path 路径

C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev0\mingw64\bin

结果验证

在控制台中如输入 gcc -v ,如返回 mingw 信息,则表示配置环境安装成功。

Go 插件安装

下载后的 eclipse 没有配置 go 的环境,需要手动配置开发系统环境.

离线安装

因为 GFC 的封闭,我们需要先到https://github.com/GoClipse/goclipse.github.io/archive/master.zip 下载 GoClipse 的安装包,并解压。然后打开 Eclipse(确保已经包含 CDT),Help –> Install New Software…,然后选择 Add 按钮,输入解压后的 Release 目录,选择 GoClipse,然后一直 next 就安装成功了。安装完成后需要重启 Eclipse。

在线安装

打开:help>Install New Software ,安装完成后,需要重启 Eclipse

Go 插件配置

打开:Window>Preferences>Go。

运行环境

首先配置 Go 的运行环境和工作空间,如下图所示。

编辑环境

此部分最快的方式就是直接下载。

go-ethereum 配置

自行到 https://github.com/ethereum/go-ethereum 下载最新版。

目录配置

请严格按照如下格式进行配置目录: src –> github.com->Ethereum->go-ethereum.

配置完成后,再次添加环境变量。

编译运行

在/go-ethereum/src/github.com/ethereum/go-ethereum/cmd/geth/main.go 中 run as, Go

applicantion. 则可以看到如下的信息输出。

引用

http://www.jianshu.com/p/d26553e7c253

http://blog.csdn.net/u013474104/article/details/52350403

感谢HPB团队整理。

HPB55:ETH基于POA的环境搭建

ETH基于POA的环境搭建

许多同学有研究POA的想法,那么今天我们尝试创建以太坊基于 POA 的环境,首先搭建环境前的准备

1.安装 go-ethereum,下载 go 的源码

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

2.编译go

1
2
3
cd go-ethereum

Make geth

3.创建4个文件夹

执行完成之后再bin的目录下会生成可执行文件geth,分别命名为node1.node2.signer1.signer2.node 是普通节点,用于后期节点间发起交易,在接下来的实验中,signer1 设置为创世块指定的授权节点;signer2为后期加入信任名单授权节点,目录如下:

1
2
3
root@test # ls

geth, node1,node2,signer1,signer2 

4.私有链搭建创建新账户,将启动的数据分别存在不同的文件夹中。

1
./geth --datadir node1/data account new

至此我们有了四个账户,四个账户分别为

1
2
3
4
5
6
7
Node1: 93ab57b1c1ae82537d6c7956d2817916166f6389

Node2: c2fdb2dffa3a14740e3d8b3baeeea504a699229f

Signer1: 7d848a70962ad3830c59ad35c6811b1fc3d07360 

Signer2:  f5413187e29113841db0ec73a2c559a53b6be7fe

5.初始化创世块

在 1.6 之后的以太坊中提供了初始化创世块的工具:puppeth, 请选择含有 Geth & Tools 的版本下载,如下图标红处。下载地址:https://geth.ethereum.org/downloads/ , 也可以按照上文自行编译。

puppeth 是个互动式的程序,直接启动照着指示输入相关信息。设置 Private chain 名称,假定为 poa_test,如下:

6.指定每隔 30 秒生成一个区块

我们指定了第一个 signer1 这个用户用来挖矿,指定了 node1和 signer1 开始被填充了余额。

7.按照如下步骤将生成的初始块进行导出

8.导出后的配置文件如下如所示:

9.创世块为 json 格式

我们将创世块的内容列举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{

"config": { "chainId": 40783,

"homesteadBlock": 1,

"eip150Block": 2, "eip150Hash":

"0x0000000000000000000000000000000000000000000000000000000000000000",

"eip155Block": 3,

"eip158Block": 3,

"byzantiumBlock": 4, "clique": {

"period": 30,

"epoch": 30000

}

},

"nonce": "0x0", "timestamp": "0x59fa93a0", "extraData":

"0x00000000000000000000000000000000000000000000000000000000000000007d848a70962a d3830c59ad35c6811b1fc3d07360000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000

0",

"gasLimit": "0x47b760", "difficulty": "0x1",

"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000",

"alloc": { "0000000000000000000000000000000000000000": {

"balance": "0x1"

},

….. // 此部分列举了部分内容,剩下为从 00 到 ff "00000000000000000000000000000000000000ff": {

"balance": "0x1"

}, "7d848a70962ad3830c59ad35c6811b1fc3d07360": {

"balance": "0x200000000000000000000000000000000000000000000000000000000000000"

}, "93ab57b1c1ae82537d6c7956d2817916166f6389": {

"balance": "0x200000000000000000000000000000000000000000000000000000000000000"

}

},

"number": "0x0",

"gasUsed": "0x0",

"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"

}

参数解释如下:

mixhash 与 nonce 配合用于挖矿,由上一个区块的一部分生成的 hash。注意他和 nonce 的设置需要满足以太坊的 Yellow paper, 4.3.4. Block Header Validity, (44)章节所描述的 条件。.
nonce nonce 就是一个 64 位随机数,用于挖矿,注意他和 mixhash 的设置需要满足以太 坊的 Yellow paper, 4.3.4. Block Header Validity, (44)章节所描述的条件。 |
difficulty 设置当前区块的难度,如果难度过大,cpu 挖矿就很难,这里设置较小难度 |
alloc 用来预置账号以及账号的以太币数量,因为私有链挖矿比较容易,所以我们不需要 预置有币的账号,需要的时候自己创建即可以。 |
coinbase 矿工的账号,随便填 |
timestamp 设置创世块的时间戳 |
parentHash 上一个区块的 hash 值,因为是创世块,所以这个值是 0 |
extraData 附加信息,在 POA 挖矿中 |
gasLimit 该值设置对 GAS 的消耗总量限制,用来限制区块能包含的交易信息总和,因为我 们是私有链,所以填最大。 |

10.初始化私有链

使用 geth init 指令,分别初始化 4 个节点的 datadir,比如初始化 node1 可执行:

1
./geth --datadir node1/data init poa_test.json

初始化完成之后,在每个文件夹中会生成两个子文件夹。

其中 geth 用来存储区块链的数据信息,keystore 用来存储账户信息,启动节点,分别执行以下命令:

1
2
3
4
5
6
7
./geth --datadir node1/data --networkid 66300 --port 3000 console

./geth --datadir node2/data --networkid 66300 --port 3001 console

./geth --datadir signer1/data --networkid 66300 --port 3002 --unlock 7d848a70962ad3830c59ad35c6811b1fc3d07360 console

./geth --datadir signer2/data --networkid 66300 --port 3003 console

11.建立data目录

datadir先前的步骤已经在每个节点各自的目录下都建立了data目录,networkid geths之间一定都要用同一个值才可以互相通信,比如实验中的66300. Portgeths 之间通信时,监听的一个port,由于四个节点都在本机,所以这里必须指定不同的值,使用 node1 对应 3000, node2 对应 3001, signer1 对应 3002, signer2 对应 3003.

Geth 参数含义如下:

identity 区块链的标示,随便填写,用于标示目前网络的名字
init 指定创世块文件的位置,并创建初始块 |
datadir 设置当前区块链网络数据存放的位置 |
port 网络监听端口 |
rpc 启动 rpc 通信,可以进行智能合约的部署和调试 |
rpcapi 设置允许连接的 rpc 的客户端,一般为 db,eth,net,web3 |
networkid 设置当前区块链的网络 ID,用于区分不同的网络,是一个数字 |
console 启动命令行模式,可以在 Geth 中执行命令 |

12.启动成功后的截图如下

因为 signer1 为挖矿节点,所以在启动的时候需要进行解锁。

13.建立节点之前的通信

目前各个节点虽然已经启动,但是各个节点之间仍然处于孤立状态。各节点启动后无法相互通信的,所以geth要连上对方的节点就必须先设置好enode://@:,复制刚刚启动node1时出现的enode信息,将[::]替换为127.0.0.1,这样就可以让其他节点加入。如上面提示的的红色提示所见, node1 的 enode 为

1
enode://720d04c0fcf239fa773493e85d3393ef3a2ce581858cbaf1756288c2decc8f08c738fabbd6b4c79fa300292755601714522fcf4febe3f6f224ea119eaac41f72@127.0.0.1:3000

在node2,singer1,singer2的geth console界面下分别运行如下指令:

完成后,在 node1 的 geth console 输入 admin.peers 应该要看到三个节点资讯,如下:(每个id 对应其他节点的 encode 字段信息)

至此四个节点之前的关系已经初步建立。

14.开始挖矿

按照实验的设计,在本实验中 signer1 为挖矿节点,我们下面开始进行模拟挖矿,在 signer1 的console 界面,键入 miner.start(),geth 就会开始挖矿了,在 signer1 的 console 会出现正在mining 的信息。

从上面可以看出挖矿的时间间隔为 30 秒,与我们创世块的中参数一致,同时在其他节点(node1 和 node2 和 signer2)则会收到 import block 的信息。如下:

15.交易转账

按照我们的预设,在 node1 和 signer1 上是已经初始化了余额的。我进行查询如下:

但是目前的账户处于锁定状态,所以首先我们要进行解锁。

进行解锁的命令如下:

1
personal.unlockAccount("0x93ab57b1c1ae82537d6c7956d2817916166f6389")

我们开始对 node2 进行转账,如下图所示:

16.查看矿机的挖矿状态

此时已经将挖到的交易打入到区块中。查看 node2 账户的余额,展示如下:

到目前为止已经完成了 POA 私有链的所有内容的搭建。

17.加入新可信节点

按照预先实验设计,signer2 也为挖矿节点,如果此时启动 signer2 节点进行挖矿,则会出现未授权的异常,如下图所示:

18.节点授权

必须回到已经在授权名单内的节点的 console 界面下,将新的节点加入。在 signer1 的 console输入指令:

此时 signer1 挖矿节点的日志已经发生变化,已经签名,等待其他签名者。

19.重新启动

此时需要以输入密码的形式重新启动 signer2

1
./geth --datadir signer2/data --networkid 66300 --port 3003 --unlock f5413187e29113841db0ec73a2c559a53b6be7fe console   

建立连接,使用 admin.addPeer()重新建立连接

1
 admin.addPeer("enode://0d2d75b5e6fd5a7e3f82da882e49b4f74206c136a241b04a2f8ba95246df 8eb55f79de9562ffe68d5582f6e0424cfea950166236a3a7cee3f3d4cd084fc33cef@127.0.0.1:3003")

20.启动 singer2 开始挖矿

可以看到 signer1 和 signer2 在交替进行挖矿。

在 40 分 36 时,singner1,挖到了一个合适的区块, 进行签名后并立即进行广播,同时 signer2 获取到了最新的区块段,签名后并提交到网络中。singner1 在收到签名后进行打包,合并到区块中,并广播最新的消息。

至此,环境搭建全部完成

引用:

https://geth.ethereum.org/downloads/

http://blog.csdn.net/sportshark/article/details/51855007

http://blog.csdn.net/aaa123524457/article/details/52836408

http://blog.csdn.net/code_segment/article/details/78160660

感谢HPB团队整理。

HPB54:以太坊交易收发机制

交易的主要数据结构

序号 数据 描述 备注
1 AccountNonce 发送者的发起的交易总数量 |
2 Recipient 交易接受者的地址 以太坊地址 |
3 Price 此次交易的 gas price |
4 GasLimit 本交易允许消耗的最大 Gas 数 |
5 Gas 要转换的 gas 数 |
6 Amount 此次交易转移的以太币数量 |
7 V 签名数据 |
8 R 签名数据 |
9 S 签名数据 |
10 Payload 其他数据 |

交易收发相关协程

交易数据验证流程

交易入池流程

感谢HPB团队整理。

HPB53:以太坊网络服务分析

网络分层

以太坊所有网络功能如下图所示:

所有网络功能建立在以太网的传输层之上,TCP 及 UDP 均有应用。

会话层

会话层主要包括 Peer 管理,NodeTable 管理和 RPC 协议,本文着重介绍 Peer 管理,

NodeTable 请参考《P2P 网络及邻居节点发现机制》。

涉及到会话层的关键代码:

源文件 类描述 主要方法
Server.go Type Server struct Server 是网络功能的基本类,所有功能均封装为 server,并进行启动。 Start:启动 server Run: 运行具体任. 务, 包括创建 Peers map, peer 拨号,peer 添加, 删除处理。Startlistening : 监 听 TCP 端口。 AddPeer: 添 加 peer RemovePeer : 移 除 Peer |
Peer.go Type Peer struct Peer 类提供各种Peer 的操作方法 NewPeer:生成一个新的 peer Run:peer 链接建立后继续进行应用层通信,并启动协议处理。 startProtocols:启动应 用层 eth 协议。 |
Dial.go Type Dialer struct Dialer 类,即拨号类,拨号即节 点向邻居节点发起 Peer 通信 Dial:实现一次拨号 |
Table.go Type Table struct 邻居节点及 K 桶相关类 |
Udp.go 提供 Kad 协议相关方法及 Peer 握手相关方法 |

Peer 介绍

Peer 指通过了通信握手的邻居节点,只有邻居节点才能变为 Peer,只有 Peer 列表中的节点,才能进行正常的通信。

Peer 管理

Peers 在代码中以 map 的结构存在,由 server 运行方法 run 创建,并在 run 方法中进行添加和删除维护。Pees 最大默认数量为 25(node/defaults.go 定义)

Peer 动态添加删除流程

Peer 添加分为两种:被动添加和主动添加。

1) 被动添加指其他节点发起握手,流程如下:

2) 每当当前 peers 有变动时,如添加,删除,或者一次 dial 任务完成,则会执行一次主动握手流程如下,其中要进行 Dial(拨号,即握手通信)的节点有以下几部分组成:

  • 静态节点,系统启动时配置文件写入
  • nodeTable 中随机选取(当前 needDynDials 的二分之一,needDynDials 的值为(s.MaxPeer+1)/2=13)
  • loobbuf 中的节点(discovery task 中的邻居节点)
  • lookbuf 中的节点 Peer 数量不足时,会强制进行一次 nodetable 刷新,刷新到的node 写入lookuf.

3) Peer 删除有三种方式: RPC 命令删除,一次应用层通信完成自动删除,通信过程读写错误。

Peer 握手机制

参考《以太坊底层技术研究:Peer 握手机制》

表示层:RLP 编码

以太坊所相关有网络上 x 发送的数据均遵循 RLP 编码,参考《RLP 机制分析》

应用层:Eth 协议

Peer握手成功后,即可进行应用层通信,协议数据包如下表所示:

序号 分类 描述
1 Code 数据包命令分类 |
2 Size Payload大小 |
3 Payload 负载数据 |
4 ReceivedAt 接受到数据包的时间,接受端节点赋值 |

ETH协议应用层包括如下命令:

序号 分类 描述
1 StatusMsg = 0x00 Handshake 使用,握 手成功后不再发送 |
2 NewBlockHashesMsg = 0x01 新区块哈希(广播) |
3 TxMsg = 0x02 交易信息(广播数据) |
4 GetBlockHeadersMsg = 0x03 获取去块头信息 |
5 BlockHeadersMsg = 0x04 获取区块头信息响应 |
6 GetBlockBodiesMsg = 0x05 获取区块信息 |
7 BlockBodiesMsg = 0x06 获取区块信息相应 |
8 NewBlockMsg = 0x07 新块信息(广播数据) |
9 GetNodeDataMsg = 0x0d 获取节点数据 |
10 NodeDataMsg = 0x0e 获取节点数据响应 |
11 GetReceiptsMsg = 0x0f 获取收据信息 |
12 ReceiptsMsg = 0x10 获取收据信息响应 |

感谢HPB团队整理。