diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe53c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.gitkeep +*.swp diff --git a/README.md b/README.md index 2a9341a..706a47f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,115 @@ -# documentation -XChainLab Documentation. +### 360区块链实验室公众号 +![qrcode](https://p2.ssl.qhimg.com/t014eaf6f590f45d97c.jpg) +360区块链实验室公众号会每周定期推送优质文章,欢迎关注~ +# 目录 +XChainLab Documentation. +- 比特币 + - [比特币源码的流程分析说明](bitcoin/比特币源码的流程分析说明.txt) + - [比特币源码的整体流程图及说明.md](bitcoin/比特币源码的整体流程图及说明.md) + - [Qt钱包的UI类图](bitcoin/Qt钱包的UI类图.md) + - [精通比特币一](bitcoin/精通比特币一.docx) + - [精通比特币二](bitcoin/精通比特币二.docx) + - [精通比特币之三钱包和交易](bitcoin/精通比特币之三钱包和交易.md) + - [精通比特币之四交易详细分析](bitcoin/精通比特币之四交易详细分析.md) + - [比特币交易源码分析](bitcoin/交易源码分析.pdf) + - [精通比特币之六区块链](bitcoin/精通比特币之六区块链.md) + - [精通比特币之五网络](bitcoin/精通比特币之五网络.md) + - [钱包的功能类图和流程图](bitcoin/钱包的功能类图和流程图.md) + - [精通比特币之七挖矿和共识](bitcoin/精通比特币之七挖矿和共识.md) +- 以太坊 + - [基于以太坊区块链的电子存证应用](ethereum/cunzheng.md) +- Hyperledger Fabric + - [Hyperledger Fabric 数据结构](fabric/Fabric.block.data.structure.md) +- EOS + - [eos源码分析之一整体介绍](eos/eos源码分析之一整体介绍.md) + - [eos源码分析之二网络](eos/eos源码分析之二网络.md) + - [eos源码分析之三交易](eos/eos源码分析之三交易.md) + - [eos源码分析之四智能合约](eos/eos源码分析之四智能合约.md) + - [eos源码分析之五虚拟机](eos/eos源码分析之五虚拟机.md) + - [eos源码分析之六共识](eos/eos源码分析之六共识.md) + - [eos源码分析之七钱包和帐户](eos/eos源码分析之七钱包和帐户.md) + - [eos源码分析之八区块及数据结构](eos/eos源码分析之八区块及数据结构.md) +- polkadot + - [波卡链源码分析之一介绍和源码模块](polkadot/波卡链源码分析之一介绍和源码模块.md) + - [波卡链源码分析之二启动流程](polkadot/波卡链源码分析之二启动流程.md) + - [波卡链源码分析之三网络](polkadot/波卡链源码分析之三网络.md) + - [波卡链源码分析之四交易](polkadot/波卡链源码分析之四交易.md) + - [波卡链源码分析之五区块及共识](polkadot/波卡链源码分析之五区块及共识.md) +- libra + - [libra的支付分析](libra/libra的支付分析.md) + - [move语言的重点分析](libra/move语言的重点分析.md) +- nebulas + - [星云链整体架构](nebulas/星云链的整体架构.md) + - [星云链并行交易分析](nebulas/星云链的并行交易分析.md) +- quorum + - [Quorum介绍](quorum/quorum.introduction.md) + - [Quorum隐私分析](quorum/quorum隐私分析.md) + - [Quorum共识算法](quorum/quorum.raft.md) + - [Quorum节点授权管理](quorum/quorum.node.permission.md) +- monoxide + - [monoxide的简单分析和讨论](monoxide/monoxide的简单分析和讨论.md) +- rsa-acc + - [RSA累加器的介绍](rsa-acc/RSA累加器的介绍.md) + - [RSA累加器的论文简要说明](rsa-acc/RSA累加器的论文简要说明.md) + - [pairing的初步](rsa-acc/pairing的初步.md) + - [一个pairing库的介绍](rsa-acc/一个pairing库的介绍.md) +- cosmos + - [cosmos源码分析之一概述](cosmos/cosmos源码分析之一概述.md) + - [cosmos源码分析之二cosmos-SDK流程](cosmos/cosmos源码分析之二cosmos-SDK流程.md) + - [cosmos源码分析之三跨链交易](cosmos/cosmos源码分析之三跨链交易.md) + - [cosmos源码分析之四staking模块](cosmos/cosmos源码分析之四Staking模块.md) + - [cosmos源码分析之五委托人delegators](cosmos/cosmos源码分析之五委托人delegators.md) + - [cosmos源码分析之六验证人Validators](cosmos/cosmos源码分析之六验证人Validators.md) + - [cosmos费用代币photon](cosmos/cosmos费用代币photon.md) + - [cosmos源码分析之七Tendermint架构分析](cosmos/cosmos源码分析之七Tendermint架构分析.md) + - [cosmos源码分析之八Tendermint启动分析](cosmos/cosmos源码分析之八Tendermint启动分析.md) +- khipu + - [khipu并行分析之一概述](khipu/khipu并行分析之一概述.md) + - [khipu并行分析之二状态和上下文](khipu/khipu并行分析之二状态和上下文.md) + - [khipu并行分析之三并行处理和验证.md](khipu/khipu并行分析之三并行处理和验证.md) +- privacy + - [门罗币、ZEC和达世币的对比分析之一总体对比](privacy/门罗币、ZEC和达世币的对比分析之一总体对比.md) + - [门罗币、ZEC和达世币的对比分析之二门罗币的环签名分析](privacy/门罗币、ZEC和达世币的对比分析之二门罗币的环签名分析.md) + - [门罗币、ZEC和达世币的对比分析之三达世币的混币分析](privacy/门罗币、ZEC和达世币的对比分析之三达世币的混币分析.md) + - [门罗币、ZEC和达世币的对比分析之四Zcash的零知识证明分析](privacy/门罗币、ZEC和达世币的对比分析之四Zcash的零知识证明分析.md) +- 扩展性 + - [plasma](scalability/plasma/plasma-in-10-mins.md) + - [跨链](scalability/Chain_Interoperability.md) + - [Making Sense of Ethereum’s Layer 2 Scaling Solutions: State Channels, Plasma, and Truebit](https://medium.com/l4-media/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit-22cb40dcc2f4) + - [跨链小总结](scalability/跨链的应用小总结.docx) +- 共识机制 + - [共识算法之:起源](consensus/consensus.basic.md) + - [共识算法之:2PC](consensus/consensus.2pc.md) + - [共识算法之:Paxos](consensus/consensus.paxos.md) + - [共识算法之:Raft](consensus/consensus.raft.md) + - [共识算法之:拜占庭将军问题](consensus/consensus.byzantine.generals.md) + - [raft](http://thesecretlivesofdata.com/raft/) + - [以太坊的PoW共识机制](consensus/ethereum.pow.md) + - [Hyperledger Fabric的PBFT共识机制](consensus/fabric.pbft.md) +- 虚拟机 + - [webassembly技术](VM/evm-ewasm/webassembly技术.md) + - [wasm文件格式](VM/evm-ewasm/wasm文件格式.md) + - [wasm的相关接口](VM/evm-ewasm/wasm的相关接口.md) + - [wasm的编译](VM/evm-ewasm/wasm的编译.md) + - [wasm的执行](VM/evm-ewasm/wasm的执行.md) + - [智能合约的bytecode分析](VM/solc/智能合约的bytecode分析.md) + - [以太坊对WASM的支持](VM/evm-ewasm/以太坊对WASM的支持.md) + - [EOS对WASM的支持](VM/evm-ewasm/EOS对WASM的支持.md) + - [wabt的一个隐性内存问题](VM/pub/wabt的一个隐性内存问题.md) +- p2p + - [架构设计](p2p/design) +- 密码学 +- 测试相关 + - [主流区块链特点](testDoc/链调研的表.xlsx) +- [相关文章翻译](translate/translate-readme.md) + - [学习使用IPFS在区块链上安全分享文件](translate/IPFS/learn-to-securely-share-files-on-the-blockchain-with-IPFS.md) + - [共识算法漫游指南](translate/consensus/A_Hitchhiker's_Guide_to_Consensus_Algorithms.md) + - [只用200行Go代码写一个自己的区块链](https://mp.weixin.qq.com/s/EQQoZp4qhLIt19PAIFhHtA) + - [200行Go代码实现自己的区块链——区块生成与网络通信](https://mp.weixin.qq.com/s/pJBnEJv7xmvkstWmkVgadQ) + - [200行Go代码实现区块链——挖矿算法](https://mp.weixin.qq.com/s/Nf7qg1nWaFYsO6x-yVyzxg) + - [使用Go语言从零编写PoS区块链](https://mp.weixin.qq.com/s/eT1lXh32bsCKpZrwxQg7ww) + - [使用Go语言编写区块链P2P网络](https://mp.weixin.qq.com/s/pHSull1QSxQWRvGyXSim5g) diff --git a/VM/EOS/EOSVMArch.md b/VM/EOS/EOSVMArch.md new file mode 100644 index 0000000..984ee2f --- /dev/null +++ b/VM/EOS/EOSVMArch.md @@ -0,0 +1,2350 @@ +# EOS智能合约和虚拟机分析 +EOS虚拟机同经典的EVM,是EOS中运行智能合约的容器,但是从设计上讲它与EOS.IO是分离的。进
+一步脚本语言和虚拟机的技术设计与EOS.IO分离。从宏观来讲任何语言或者虚拟机,只要满足条件适
+合沙盒模式运行,同时满足一定的运行效率,都可以通过满足EOS.IO提供的API来加入到EOS.IO的消
+息传递过程中。以下为github上官方的说明:
+>The EOS.IO software will be first and foremost a platform for coordinating
+ the delivery of authenticated messages (called Actions) to accounts. The details
+ of scripting language and virtual machine are implementation specific details
+ that are mostly independent from the design of the EOS.IO technology. Any
+ language or virtual machine that is deterministic and properly sandboxed with
+ sufficient performance can be integrated with the EOS.IO software API. + +本文就EOSIO中的智能合约和虚拟机进行分析来从更加全面的角度来看EOS是如何构建和实现。 + +- [EOS智能合约和虚拟机分析](#eos) + - [相关背景知识](#) + - [LLVM相关内容](#llvm) + - [LLVM架构概述](#llvm-1) + - [LLVM IR介绍与分析](#llvm-ir) + - [LLVM IR格式](#llvm-ir-1) + - [LLVM IR指令集](#llvm-ir-2) + - [LLVM IR类型系统](#llvm-ir-3) + - [LLVM IR内存模型](#llvm-ir-4) + - [LLVM IR函数调用](#llvm-ir-5) + - [LLVM IR示例](#llvm-ir-6) + - [LLVM JIT介绍与分析](#llvm-jit) + - [LLVM JIT实现原理](#llvm-jit-1) + - [LLVM JIT代码示例](#llvm-jit-2) + - [WebAssembly相关内容](#webassembly) + - [WebAssembly概述](#webassembly-1) + - [WebAssembly格式介绍与分析](#webassembly-2) + - [WebAssembly WAST格式介绍](#webassembly-wast) + - [WebAssembly WASM格式介绍](#webassembly-wasm) + - [WASM运行介绍与分析](#wasm) + - [EOS智能合约分析](#eos-1) + - [EOS智能合约概览](#eos-2) + - [EOS智能合约模型和执行流程](#eos-3) + - [EOS智能合约与Action](#eosaction) + - [EOS智能合约执行流程](#eos-4) + - [inline Communication](#inline-communication) + - [Deferred Communication](#deferred-communication) + - [执行流程示例](#-1) + - [EOS智能合约示例说明](#eos-5) + - [EOS智能合约相关工具](#eos-6) + - [EOS虚拟机分析](#eos-7) + - [EOS虚拟机概览](#eos-8) + - [EOS虚拟机实现思路分析](#eos-9) + - [EOS虚拟机架构概述](#eos-10) + - [EOS虚拟机实现与分析](#eos-11) + - [EOS虚拟机核心接口](#eos-12) + - [EOS虚拟机架构应用层](#eos-13) + - [EOS虚拟机客户端合约部署](#eos-14) + - [EOS虚拟机服务端合约部署](#eos-15) + - [EOS虚拟机服务端合约的调用执行](#eos-16) + - [EOS虚拟机Module IR生成](#eosmodule-ir) + - [VirtualMachine实例化](#virtualmachine) + - [Binaryen底层解释器](#binaryen) + - [ModuleInstance的创建](#moduleinstance) + - [Appply接口的实现和调用](#appply) + - [CallFunction的实现](#callfunction) + - [WAVM底层解释器](#wavm) + - [ModuleInstance的生成](#moduleinstance-1) + - [Apply接口实现和调用](#apply) + - [InvokeFunction的实现](#invokefunction) + - [总结](#-2) + + + +## 相关背景知识 +### LLVM相关内容 +**LLVM相关技术的理解对于我们深入理解EOS虚拟机的运行机制至关重要**,所以必要的LLVM的相关知
+识在这里是需要的。同时LLVM作为一个成熟的编译器后端实现,无论从架构还是相关设计思想以及相
+关的工具的实现都是值得学习的。 +#### LLVM架构概述 +概括来讲LLVM项目是一系列分模块、可重用的编译工具链。它提供了一种代码良好的中间表示(IR),
+LLVM实现上可以作为多种语言的后端,还可以提供与语言无关的优化和针对多种CPU的代码生成功能。
+最初UIUC的Chris Lattner主持开发了一套称为LLVM(Low Level Virtual Machine)的编译器工具库套
+件,但是后来随着LLVM的范围的不断扩大,则这个简写并不代表底层虚拟机的含义,而作为整个项目
+的正式名称使用,并一直延续至今。所以现在的LLVM并不代表Low Level Virtual Machine。 +>The LLVM Project is a collection of modular and reusable compiler and toolchain
+technologies. Despite its name, LLVM has little to do with traditional virtual machines.
+The name "LLVM" itself is not an acronym; it is the full name of the project. + +LLVM不同于传统的我们熟知的编译器。传统的静态编译器(如gcc)通常将编译分为三个阶段,分别
+由三个组件来完成具体工作,分别为前端、优化器和后端,如下图所示。
+![CommonComplier](./image/CommonComplier.png "MainClass")
+LLVM项目在整体上也分为三个部分,同传统编译器一致,如下图所示,不同的语言的前端,统一的
+优化器,以及针对不同平台的机器码生成。从图2我们也可以得到启发,如果想实现一门自定义的
+语言,目前主要的工作可以集中在如何实现一个LLVM的前端上来。
+![LLVMArchSimple](./image/LLVMArchSimple.png "MainClass")
+LLVM的架构相对于传统编译器更加的灵活,有其他编译器不具备的优势,从LLVM整体的流程中我
+们就可以看到这一点,如下图所示为LLVM整体的流程,编译前端将源码编译成LLVM中间格式的文
+件,然后使用LLVM Linker进行链接。Linker执行大量的链接时优化,特别是过程间优化。链接得
+到的LLVM code最终会被翻译成特定平台的机器码,另外LLVM支持JIT。本地代码生成器会在代码
+生成过程中插入一些轻量级的操作指令来收集运行时的一些信息,例如识别hot region。运行时收
+集到的信息可以用于离线优化,执行一些更为激进的profile-driven的优化策略,调整native code
+以适应特定的架构。
+![LLVMArchFull](./image/LLVMArchFull.jpg "MainClass")
+从图中我们也可以得出LLVM突出的几个优势: +* 持续的程序信息,每个阶段都可以获得程序的信息内容 +* 离线代码生成,产生较高的可执行程序 +* 便捷profiling及优化,方便优化的实施 +* 透明的运行时模型 +* 统一,全程序编译 + +#### LLVM IR介绍与分析 +根据编译原理可知,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,即
+"IR"。之后再由中间语言,利用后端程序翻译为目标平台的汇编语言。由于中间语言相当于一款编
+译器前端和后端的“桥梁”,不同编译器的中间语言IR是不一样的,IR语言的设计直接会影响到编
+译器后端的优化工作。LLVM IR官方介绍见:http://llvm.org/docs/LangRef.html
+ +##### LLVM IR格式 +>The LLVM code representation is designed to be used in three different forms: as an
+in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading
+by a Just-In-Time compiler), and as a human readable assembly language representation.
+This allows LLVM to provide a powerful intermediate representation for efficient compiler
+transformations and analysis, while providing a natural means to debug and visualize the
+transformations. + +由上诉的引用得知目前LLVM IR提供三种格式,分别是内存里面的IR模型,存储在磁盘上的二进制
+格式,存储在磁盘上的文本可读格式。三者本质上没有区别,其中二进制格式以bc为文件扩展名,
+文本格式以ll为文件扩展名。除了以上两个格式文件外,和IR相关的文件格式还有s和out文件,这
+两种一个是由IR生成汇编的格式文件,一个是生成的可执行文件格式(linux下如ELF格式), +* bc结尾,LLVM IR文件,二进制格式,可以通过lli执行 +* ll结尾,LLVM IR文件,文本格式,可以通过lli执行 +* s结尾,本地汇编文件 +* out, 本地可执行文件 + +以上几种不同文件的转化图如下所示,整体上我们可以看一下这几种格式的转化关系,同时从中
+我们也可以看出工具clang、llvm-dis、llvm-as等工具的作用和使用。
+![LLVMFormatFileTrans](./image/LLVMFormatFileTrans.jpg "MainClass")
+中间语言IR的表示,一般是按照如下的结构进行组织的由外到内分别是:
+* 模块(Module) +* 函数(Function) +* 代码块(BasicBlock) +* 指令(Instruction) + +模块包含了函数,函数又包含了代码块,后者又是由指令组成。除了模块以外,所有结构都是从
+值产生而来的。如下为一个ll文件的片段,从中可以简单的看出这种组织关系。
+```c++ +; ModuleID = 'main.ll' +source_filename = "main.c" +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" + +; Function Attrs: noinline nounwind uwtable +define i32 @add(i32, i32) #0 { + %3 = alloca i32, align 4 + %4 = alloca i32, align 4 + store i32 %0, i32* %3, align 4 + store i32 %1, i32* %4, align 4 + %5 = load i32, i32* %3, align 4 + %6 = load i32, i32* %4, align 4 + %7 = add nsw i32 %5, %6 + ret i32 %7 +} + +``` +##### LLVM IR指令集 +指令集的分类大致可以分为基于栈的,基于运算器的还有基于寄存器的,基于栈的和基于寄存器
+的虚拟机目前是比较常见的,两种不同之处主要在运行效率,指令集大小和性能三个方面。LLVM
+IR采用的是基于寄存器的满足RISC架构以及load/store模式,也就是说只能通过将load和store
+指令来进行CPU和内存间的数据交换。LLVM IR指令集拥有普通CPU一些关键的操作,屏蔽掉了
+一些和机器相关的一些约束。LLVM提供了足够多的寄存器来存储基本类型值,寄存器是为SSA形
+式(静态单态赋值),这种形式的UD链(use-define chain, 赋值代表define, 使用变量代表use)
+便于优化。LLVM指令集仅包含31条操作码。LLVM中的内存地址没有使用SSA形式,因为内存地
+址有可能会存在别名或指针指向,这样就很难构造出来一个紧凑可靠的SSA表示。在LLVM中一个
+function就是一组基本块的组合,一个基本块就是一组连续执行的指令并以中指指令结束
+(包括branch, return, unwind, 或者invoke等),中止指令指明了欲跳转的目的地址。
+ +##### LLVM IR类型系统 +LLVM的类型系统为语言无关。每一个SSA寄存器或者显示的内存对象都有其对应的类型。这些类
+型和操作码一起表明这个操作的语义,这些类型信息让LLVM能够在低层次code的基础上进行一
+些高层次的分析与转换,LLVM IR包含了一些语言共有的基本类型,并给他们一些预定义的大小,
+从8bytes到64bytes不等,基本类型的定义保证了LLVM IR的移植性。同时LLVM又包含了四种复杂
+类型,pointer,arrays, structures和functions。这四种类型足够表示现有的所有语言类型。为
+了支持类型转换,LLVM提供了一个cast操作来实现类型的转换,同时为了支持地址运算,LLVM
+提供了getelementptr的命令。LLVM中的许多优化都是基于地址做的(后续的总结再分析)。 + +##### LLVM IR内存模型 +LLVM提供特定类型的内存分配,可以使用malloc指令在堆上分配一个或多个同一类型的内存对象,
+free指令用来释放malloc分配的内存(和C语言中的内存分配类似)。另外提供了alloca指令用于
+在栈上分配内存对象,该内存对象在通常在函数结尾会被释放。统一内存模型,所有能够取地址的
+对象都必须显示分配。局部变量也要使用alloca来显示分配,没有隐式地手段来获取内存地址,这就
+简化了关于内存的分析。 + +##### LLVM IR函数调用 +LLVM中对普通函数调用,LLVM提供了call指令来调用附带类型信息的函数指针。这种抽象屏蔽了
+机器相关的调用惯例。还有一个不能忽略的就是异常处理,在LLVM中,LLVM提供了invoke和
+unwind指令。invoke指令指定在栈展开的过程中必须要执行的代码,例如栈展开的时候需要析构
+局部对象等。而unwind指令用于抛出异常并执行栈展开的操作。栈展开的过程会被invoke指令停
+下来,执行catch块中的行为或者执行在跳出当前活动记录之前需的操作。执行完成后继续代码执
+行或者继续栈展开操作。注意像C++的RTTI则由C++自己的库处理,LLVM并不负责。 + +##### LLVM IR示例 +下面我们编写一个简短的程序并编译成LLVM IR的形式来看LLVM的IR的具体格式和结构如下为一
+段程序,保存为main.c
+```c +#include +int add(int a, int b) +{ + return (a + b); +} +int main(int argc, char** argv) +{ + add(3, 5); + return 0; +} +``` +我们使用命令***clang -o0 -emit-llvm main.c -S -o main.ll***编译生成ll文件,ll文件为文本可见
+文件,内容如下:
+```c++ +; ModuleID = 'main.c' +source_filename = "main.c" +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" +//函数特征如inline +; Function Attrs: noinline nounwind uwtable +define i32 @add(i32, i32) #0 { //@代表是全局属性 i32为数据类型 +%3 = alloca i32, align 4 //申请空间存放变量,%为局部属性 +%4 = alloca i32, align 4 //3,4用来存放传入的参数,aling为位宽 +store i32 %0, i32* %3, align 4 //将传入的参数放到是对应的存储位置 +store i32 %1, i32* %4, align 4 +%5 = load i32, i32* %3, align 4 //将参数存到待运算的临时变量中 +%6 = load i32, i32* %4, align 4 +%7 = add nsw i32 %5, %6 //执行具体的相加操作 +ret i32 %7 //最后返回结果 +} +; Function Attrs: noinline nounwind uwtable +define i32 @main(i32, i8**) #0 { +%3 = alloca i32, align 4 +%4 = alloca i32, align 4 +%5 = alloca i8**, align 8 +store i32 0, i32* %3, align 4 +store i32 %0, i32* %4, align 4 +store i8** %1, i8*** %5, align 8 +%6 = call i32 @add(i32 3, i32 5) +ret i32 0 +} +``` +以上代码不难发现函数add的展开中有部分临时变量的浪费,更为简洁的表达可以如下,当然
+际的优化到什么程度要看后续的具体的实现。 +``` +%3 = add nsw i32 %1, %0 +ret i32 %3 +``` + +#### LLVM JIT介绍与分析 +JIT技术Just-In-Time Compiler,是一种动态编译中间代码的方式,根据需要,在程序中编
+译并执行生成的机器码,能够大幅提升动态语言的执行速度。LLVM设计上考虑了解释执行
+的功能,这使它的IR可以跨平台去使用,代码可以方便地跨平台运行,同时又具有编译型语言
+的优势,非常的方便。像Java语言,.NET平台等,广泛使用JIT技术,使得程序达到了非常
+高的执行效率,逐渐接近原生机器语言代码的性能。 +##### LLVM JIT实现原理 +JIT引擎的工作原理并没有那么复杂,本质上是将原来编译器要生成机器码的部分要直接写
+入到当前的内存中,然后通过函数指针的转换,找到对应的机器码并进行执行。实际编写
+过程中往往需要处理例如内存的管理,符号的重定向,处理外部符号等问题。实现一个LLVM
+的字节码(bc)的解释器其实并不复杂最好的实例就是LLVM自身的解释器lli,其总共不超过
+800行代码实现了一个LLVM的字节码解释器,其源代码的github地址为:
+https://github.com/llvm-mirror/llvm/blob/master/tools/lli/lli.cpp
+ +##### LLVM JIT代码示例 +下面就以LLVM源代码中的例子来解释LLVM-JIT是如何使用和运行的,在这之前,我们需
+要明确llvm中常用的语句表达结构为module-->function-->basicblock-->instruction
+-->operator
我们主要分析源代码example/HowToUseJIT部分的代码,主要代码片段如下:
+该例子中在内存中创建了一个LLVM的module,这个module包含如下两个function:
+```c++ +int add1(int x) { + return x+1; +} +int foo() { + return add1(10); +} +``` +针对以上两个函数,创建LLVM内存中IR中间格式的代码如下:
+```C++ +//首先包含llvm JIT需要的相关头文件 +#include "llvm/ADT/STLExtras.h" +#include "llvm/ExecutionEngine/ExecutionEngine.h" +#include "llvm/ExecutionEngine/GenericValue.h" +............... +............... +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include +using namespace llvm; + +int main() { + InitializeNativeTarget(); //初始化本地执行环境,和具体的机器相关 + LLVMContext Context; //定义一个LLVM的上下文变量 + //创建一个module对象,以便后续我们可以把function放入其中 + //这里这个module对象的名字是text,关联的上下文为上面声明 + std::unique_ptr Owner = make_unique("test", Context); + Module* M = Owner.get(); + //创建add1函数对象,并把该对象加入到module中, + Function* Add1F = cast(M->getOrInsertFunction( + "add1", //函数的名字为add1 + Type::getInt32Ty(Context),//函数的参数为int32 + Type::getInt32Ty(Context))); //函数的返回值为int32 + //创建一个块,并把块关联到add1函数上,注意函数的最后一个参数 + BasicBlock* BB = BasicBlock::Create(Context, "EntryBlock", Add1F); + //创建一个basic block的builder,这个builder的工作就是将instructions添加到 + //basic block中去 + IRBuilder<> builder(BB); + //获得一个指向常量数字1的指针 + Value* One = builder.getInt32(1); + //获得指向函数add1第一个参数的指针 + assert(Add1F->arg_begin() != Add1F->arg_end()); // 确保有参数 + Argument* ArgX = &* Add1F->arg_begin(); // 获得参数指针 + ArgX->setName("AnArg"); + // 设置参数名称,便于后续的查找 + //创建加1的指令,并把指令放入到块的尾部 + Value* Add = builder.CreateAdd(One, ArgX); + //创建返回指令, 至此add1的函数已经创建完毕 + builder.CreateRet(Add); + //创建函数foo + Function* FooF = cast(M->getOrInsertFunction( + "foo", Type::getInt32Ty(Context))); + BB = BasicBlock::Create(Context, "EntryBlock", FooF); + //通知builder关联到一个新的block上 + builder.SetInsertPoint(BB); + Value* Ten = builder.getInt32(10); + //创建一个函数的调用,并把参数传递进去 + CallInst* Add1CallRes = builder.CreateCall(Add1F, Ten); + Add1CallRes->setTailCall(true); + //创建返回结果 + builder.CreateRet(Add1CallRes); + // 创建JIT引擎,创建参数为上下文 + ExecutionEngine* EE = EngineBuilder(std::move(Owner)).create(); + outs() << "We just constructed this LLVM module:\n\n" << * M; + outs() << "\n\nRunning foo: "; + outs().flush(); + //调用函数foo + std::vector noargs; + GenericValue gv = EE->runFunction(FooF, noargs); + //获得函数返回值 + outs() << "Result: " << gv.IntVal << "\n"; + delete EE; + //关闭LLVM虚拟机 + llvm_shutdown(); + return 0; +} +``` + + +以上代码在内存中创建了LLVM IR,并调用LLVM JIT的执行引擎运行代码,从中我们得到启
+发是如果我们借助LLVM JIT运行我们的合约代码,我们就需要将合约代码最终转化为LLVM
+能识别的中间代码IR上,下面将一步一步的分析EOS中是如何利用LLVM-JIT技术实现的虚
+拟机运行。 + + +### WebAssembly相关内容 +#### WebAssembly概述 +WASM在浏览器中运行的效果和Java语言在浏览器上的表现几近相同的时候,但是WASM
+不是一种语言,确切的说WASM是一种技术方案,该技术方案允许应用诸如C、C++这种
+编程语言编写运行在web浏览其中的程序。更加细节的去讲,WASM是一种新的字节码格
+式,是一种全新的底层二进制语法。突出的特点就是精简,加载时间短以及高速的执行模
+型。还有一点比较重要,那就是它设计为web多语言编程的目标文件格式。具体可见官网
+相关介绍:https://webassembly.org/
+![WASMToHTMLFlow](./image/WASMToHTMLFlow.jpg "MainClass") + +#### WebAssembly格式介绍与分析 +WebAssembly同LLVM的IR类似,提供两种格式,分别为可读的文本格式wast和二进
+制格式wasm,两者最终是等价的,可以通过工具wast2wasm完成wast到wasm的格式转
+而工具wasm2wast则执行这一过程的返作用。 +##### WebAssembly WAST格式介绍 +为了能够让人阅读和编辑WebAssembly,wasm二进制格式提供了相应的文本表示。这
+是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。下面将用基本
+语法的方式解释了这种文本表示是如何工作的以及它是如何与它表示的底层字节码。 + +无论是二进制还是文本格式,WebAssembly代码中的基本单元是一个模块。在文本格式
+中,一个模块被表示为一个S-表达式。S-表达式是一个非常古老和非常简单的用来表示树
+的文本格式。具体介绍:https://en.wikipedia.org/wiki/S-expression 因此,我们可以
+把一个模块想象为一棵由描述了模块结构和代码的节点组成的树。与编程语言的抽象语
+法树不同的是,WebAssembly的树是平坦的,也就是大部分包含了指令列表。树上的
+每个一个节点都有一对括号包围。括号内的第一个标签表示该节点的类型,其后跟随的
+是由空格分隔的属性或孩子节点列表。因此WebAssembly的S表达式结构大概如下所示:
+>(module (memory 1) (func)) + +上面的表达式的含义是模块module包含两个孩子节点,分别是属性为1的内存节点,和
+函数func节点。从上面我们知道一个空的模块定义为`module`,那将一个空的模块转化为
+wasm将是什么格式,如下所示:
+>0000000: 0061 736d ; WASM_BINARY_MAGIC
+0000004: 0d00 0000 ; WASM_BINARY_VERSION + +WebAssembly模块中的所有代码都是包含函数里面。函数的结构如下所示:
+```c++ +( func [signature] [locals] [body] ) +``` +* signature 函数的签名声明函数的参数和返回值 +* local 局部变量,声明了具体的类型 +* body 为函数体,一个低级的的指令的线性列表 + +关于数据类型这里简单说明一下,wasm目前有四种可用的数据类型,分别为i32 i64 f32 f64
+关于**签名**我们来看一个签名的具体例子,如下所示表示函数需要两个参数,均为i32类型,
+返回值是一个f64类型,参数可以看成是函数调用过程中传递过来的实参初始化后的局部变量。
+```c++ +(func (param i32) (param i32) (result f64) ... ) +``` +关于**局部变量**这里需要注意两个操作:get_local和set_local,先看下面的例子: +```c++ +(func (param i32) (param f32) (local f64) get_local 0 get_local 1 get_local 2) +``` +* get_local 0会得到i32类型的参数 +* get_local 1会得到f32类型的参数 +* get_local 2会得到f64类型的局部变量 + +为了便于识记,可以定义变量名的方式来取代索引的方式,具体如下:
+```c++ +(func (param $p1 i32) (param $p2 f32) (local $loc i32) …) +``` +关于**函数体**,在具体介绍函数体之前,我们要明确的一点是,虽然wasm被设计成高效执行
+的代码,但是最后wasm的执行依然是一个栈式机器定义的,下面我们参考如下代码:
+```c++ +(func (param $p i32) ..get_local $p get_local $p i32.add) +``` +上面函数的功能概括为i+i,即计算表达是$p+$p的结果,结果将放在最后运行的栈的顶部。
+现在我们完整的写出一个module,该module就包含上述的功能,具体的S表达式如下:
+```c++ +(module + (func (param $lhs i32) (param $rhs i32) (result i32) + get_local $lhs + get_local $rhs + i32.ad + ) +) +``` +上面的描述似乎缺少了什么,那就我们如何才能使用这个函数,于是涉及到函数的导出和调用。
+wasm中是通过export来完成导出的,通过call关键字来完成函数调用的,如下一个更加复杂
+的例子: +```c++ +(module + (func $getNum (result i32) + i32.const 42) + (func (export "getPlus") (result i32) + call $getNum + i32.const 1 + i32.add + ) +) +``` +函数运行最后的结果在栈顶保存43这个元素,注意其中的(export "getPlus")也可以通过如下的
+方式`(export "getPlus" (func $getPlus))`的方式导出。最后一个问题wasm如何导入函数?
+下面我们看一个具体的例子 : +```c++ +(module
+ (import "console" "log" (func $log (param i32))) + (func (export "logIt") + i32.const 13 + call $log)) +``` +WebAssembly使用了两级命名空间,这里的导入语句是说我们要求从console模块导入log函
+数。导出的logIt函数使用call指令调用了导入的函数。
+**小结:** 到目前为止我们熟悉了wast的具体格式,关于wast中的外部内存使用,表格等高级内容
+可以单独去了解。 +##### WebAssembly WASM格式介绍 +wasm为WebAssembly的二进制格式,可以通过工具wast2wasm将wast转化为wasm格式,下
+面将如下wast转化为wasm, 命令为`wat2wasm simple.wast -o simple.wasm`
+上述工具的地址为:https://github.com/WebAssembly/wabt/
+```c++ +(module + (func $getNum (result i32) + i32.const 42) + (func (export "getPlus") (result i32) + call $getNum + i32.const 1 + i32.add + ) +) +``` +虽然编译好的二进制文件没有办法进行直观的读取,但是可以借助wat2wasm工具进行查看其
+verbose的输出,命令为:`./wat2wasm test.wat -v`输出结果为如下,通过对如下字节流的理
+我们可以清晰看到wasm的二进制流格式是什么样的,以及它是如何运行的。基于以下的代码我
+可以自己构建一个wasm的解析引擎,引擎需要使用寄存器的设计加上栈的运行控制。 +```c++ +0000000: 0061 736d ; WASM_BINARY_MAGIC +0000004: 0100 0000 ; WASM_BINARY_VERSION +; section "Type" (1) +0000008: 01 ; section code +0000009: 00 ; section size (guess) +000000a: 01 ; num types +; type 0 +000000b: 60 ; func +000000c: 00 ; num params +000000d: 01 ; num results +000000e: 7f ; i32 +0000009: 05 ; FIXUP section size +; section "Function" (3) +000000f: 03 ; section code +0000010: 00 ; section size (guess) +0000011: 02 ; num functions +0000012: 00 ; function 0 signature index +0000013: 00 ; function 1 signature index +0000010: 03 ; FIXUP section size +; section "Export" (7) +0000014: 07 ; section code +0000015: 00 ; section size (guess) +0000016: 01 ; num exports +0000017: 07 ; string length +0000018: 6765 7450 6c75 73 getPlus ; export name +000001f: 00 ; export kind +0000020: 01 ; export func index +0000015: 0b ; FIXUP section size +; section "Code" (10) +0000021: 0a ; section code +0000022: 00 ; section size (guess) +0000023: 02 ; num functions +; 上面的代码基本上都声明和签名,如下代码才是真正的函数体代码 +; function body 0 +0000024: 00 ; func body size (guess) +0000025: 00 ; local decl count +0000026: 41 ; i32.const +0000027: 2a ; i32 literal +0000028: 0b ; end +0000024: 04 ; FIXUP func body size +; function body 1 +0000029: 00 ; func body size (guess) +000002a: 00 ; local decl count +000002b: 10 ; call +000002c: 00 ; function index +000002d: 41 ; i32.const +000002e: 01 ; i32 literal +000002f: 6a ; i32.add +0000030: 0b ; end +0000029: 07 ; FIXUP func body size +0000022: 0e ; FIXUP section size +``` +这里我们要注意一点是wasm中不同section是有一定的排序的,具体的顺序如下 +```c+++ +user 0 +type 1 +import 2 +functionDeclarations 3 +table 4 +memory 5 +global 6 +export 7 +start 8 +elem 9 +functionDefinitions 10 +data 11 +``` +#### WASM运行介绍与分析 +wasm目前主要的应用领域在于web应用,对于EOS其将作为智能合约的最终格式,其目前运行
+在WAVM上,其机制不同于目前浏览的运行和调用方式。首先我们先简单了解一下wasm是如
+在浏览器中运行,而WAVM的运行时分析将在EOS虚拟机中进行。
+浏览器运行的示例:https://webassembly.org/getting-started/developers-guide/
+这里可以看到利用emcc的工具生成的最终代码,其中主要有wasm文件,js胶水文件和html
+调用文件。
+![WASMInWebBrowser](./image/WASMInWebBrowser.png "MainClass") +## EOS智能合约分析 +### EOS智能合约概览 +EOS中的智能合约概括的来讲就是对多个输入来组织商议输出的过程,EOS中的合约不仅仅
+可以实现例如转账的这种经济行为,也可以描述游戏规则。EOS中的合约作为注册在EOS区
+块链上的应用程序并最终运行在EOS的节点上。EOS的智能合约定义了相关的接口,这些接
+口包含action,数据结构和相关的参数,同时智能合约实现这些接口,最后被编译成二进制格
+式,在EOS中为wasm,节点负责解析字节码来执行对应的智能合约。对于区块链而言,最
+终存储的是智能合约的交易(transactions)。 +### EOS智能合约模型和执行流程 +EOS中的智能合约由两个部分组成分别为action集合和类型的定义: +* action集合,定义和实现了智能合约的行为和功能 +* 类型定义,定义了合约需要的内容和数据结构 + +#### EOS智能合约与Action +EOS中的action操作构建与一个消息架构之上,客户端通过发送消息来触发action的执行,
+我们知道智能合约最终的存储形式是一个transaction,那transaction和action之间是什么关
+系,在这里一个transaction包含至少一个action,而一个action代表的是大一的操作。如下为
+一个包含多个action的transaction。对于如下的transaction,当其中所有的action都成功的
+时候,这个transaction才算成功。如果一个transaction成功后,则其receipt生成,但是此时
+并不代表transaction已经确认,只是说明确认的概率大一些 +```c++ +{ + "expiration": "...", + "region": 0, + "ref_block_num": ..., + "ref_block_prefix": ..., + "net_usage_words": .., + "kcpu_usage": .., + "delay_sec": 0, + "context_free_actions": [], + "actions": [{ + "account": "...", + "name": "...", + "authorization": [{ + "actor": "...", + "permission": "..." + } + ], + "data": "..." + }, { + "account": "...", + "name": "...", + "authorization": [{ + "actor": "...", + "permission": "..." + } + ], + "data": "..." + } + ], + "signatures": [ + "" + ], + "context_free_data": [] +} +``` +EOS的智能合约提供一个action handler来完成对action的请求,每次一个action执行在实现
+上通过调用apply方法,EOSIO通过创建一个apply的上下文来辅助action的执行,如下的图
+说明一个apply上下文的关键元素。
+![EOSApplyCTX](./image/EOSApplyCTX.png "MainClass") + +从全局的角度看,EOS区块链中的每个节点将获得智能合约中每个action的一个拷贝,在
+所有节点的运行状态中,一些节点在执行智能合约的实际工作,而一些节点在做交易的验
+证,因此对于一个合约来说能比较重要的一点就是知道当前运行的实际的上下文是什么,
+也就是说目前处在哪个阶段,在这里上下文的标识被记录在action的上下文中来完成上面
+的工作,如上面图所示这个上下文标识包括三个部分,分别是reciver,code和action。
+receiver表示当前处理这个action的账户,code代表授权了这个合约账户,而action是
+当前运行的action的ID。
+根据上面我们知道transaction和action的关系,如果一个transaction失败,所有在这个
+transaction中的action的计算结果都需要被回滚,在一个action上下文中一个关键的数据
+成员就是当前的transaction数据,它包含以下几个部分:
+* transaction的头 +* 包含transaction中所有的原始的action的容器,容器已经排好序 +* 包含transaction中的上下文无关的action的容器 +* 一个可以删节的上下文无关的数据,这部分数据是被合约定义的,以一个二进制长
+对象集合提供 +* 对上述二进制长对象的索引 + +在EOS中每个action执行的时候都会重新的申请一块新的内存,每个action上下文中的变量是
+私有的,即使在同一个transaction中的action,他们的变量也是不可以共享,唯一的一种方式
+来共享变量就是通过持久化数据到EOS的数据库中,这些可以通过EOSIO的持久化API来实现。 +#### EOS智能合约执行流程 +EOS中的智能合约彼此可以进行通讯,例如一个合约调用另外的合约来完成相关操作来完成当
+前的transaction,或者去触发一个当前transaction的scope外的一个外来的transaction。
+EOS中支持两种不基本的通讯模型,分别是inline和deferred两种,典型的在当前transaction
+中的操作是inline的方式的action的实例,而被触发的一个将要执行的transaction则是一个deferred
+action的实例。在智能合约之间的通讯我们可以看做是异步的。 +##### inline Communication +Inline的通讯模式主要体现在对需要执行的action的请求过程直接采用调用的方式,Inline方式
+下的action在同一transaction的scope和认证下,同时action被组织起来用于执行当前的transaction
+Inline action可以被看做是transaction的嵌套,如果transaction的任何一个部分执行失败,那么
+inline action也只会在transaction的剩下部分展开, 调用inline action不会产生任何对外的通知
+无论其中是成功还是失败,综上也就是说inline action的作用范围是在一个transaction中的。 + +##### Deferred Communication +Deferred的通讯模式采用的是通过通知另一个节点transaction的方式来实现的。一个Deferred
+actions一般会稍后调用,对于出块生产者来说并不保证其执行。对于创造Deferred action的
+transaction来说它只能保证是否创建和提交成功,对于是否执行成功与否无法保证。对于一个
+Deferred action来说其携带合约的验证信息进行传递。特殊的一个transaction可以取消一个
+deferred的transaction。 +##### 执行流程示例 +如下如未EOS wiki上给出的一个包含inline action的transaction的执行流程。
+![ExampleOfTF](./image/ExampleOfTF.png "MainClass")
+ +从图中我们可以看到,这个transaction中有两个inline action,分别是 +* employer::runpayroll +* employer::dootherstuff + +由上面的图,我们可以很清晰的知道,action通过调用inline action并递归的调用最后来完成
+整个transactio的执行。同上对于上面的一个转账发薪酬的场景也可以通过Deferred的方式
+来完成,如下图所示:
+![ExampleOfDTF](./image/ExampleOfDTF.png "MainClass")
+### EOS智能合约示例说明 +EOS智能合约一般用c++语言实现,可以通过工具来进行编译成最后的智能合约二进制码,一
+段典型的智能合约代码如下: +```c++ +#include + +using namespace eosio; + +class hello : public eosio::contract { + public: + using contract::contract; + /// @abi action + void hi( account_name user ) { + print( "Hello, ", name{user} ); + } +}; + +EOSIO_ABI( hello, (hi) ) +``` +对于每一个智能合约而言,其必须提供一个apply的接口,这个接口函数需要监听所有输入的aciton
+并作出对应的动作,apply用recevier,code和action来过来输入并执行特定的操作。形式如下:
+```c++ +if (code == N(${contract_name}) { + // your handler to respond to particular action +} +``` +EOS中的的宏EOSIO_ABI屏蔽了底层实现的细节,宏展开如下所示: +```c++ +#define EOSIO_ABI( TYPE, MEMBERS ) \ +extern "C" { \ + void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \ + auto self = receiver; \ + if( action == N(onerror)) { \ + eosio_assert(code == N(eosio), \ + "onerror action's are only valid from the \"eosio\" system account"); \ + } \ + if( code == self || action == N(onerror) ) { \ + TYPE thiscontract( self ); \ + switch( action ) { \ + EOSIO_API( TYPE, MEMBERS ) \ + } \ + /* does not allow destructor of thiscontract to run: eosio_exit(0); * / \ + } \ + } \ +} \ + +``` +其中EOSIO_ABI的宏定义如下: +```c++ +#define EOSIO_API( TYPE, MEMBERS ) \ + BOOST_PP_SEQ_FOR_EACH( EOSIO_API_CALL, TYPE, MEMBERS ) +``` +我们继续展开宏EOSIO_API_CALL如下: +```c++ +#define EOSIO_API_CALL( r, OP, elem ) \ + case ::eosio::string_to_name( BOOST_PP_STRINGIZE(elem) ): \ + eosio::execute_action( &thiscontract, &OP::elem ); \ + break; +``` +这样我们就明确一个只能合约被调用的时候最后是如何反应到代码层面进行路由调用的。
+### EOS智能合约相关工具 +由上文我们知道一个智能合约源文件大概的样子,现在我们来看一下如何生成EOS虚拟机支持的格
+式。EOS虚拟机目前支持加载wast和wasm两种格式的智能合约。现在看下EOS中智能合约是如何
+构建的,如下代码为tools/eosiocpp.in中关于合约的编译脚本,其中省略部分非关键代码:
+```shell +function build_contract { +($PRINT_CMDS; @WASM_CLANG@ -emit-llvm -O3 --std=c++14 --target=wasm32 -nostdinc \ + -nostdlib -nostdlibinc -ffreestanding -nostdlib -fno-threadsafe-statics -fno-rtti \ + -fno-exceptions -I ${EOSIO_INSTALL_DIR}/include \ + -I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \ + -I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \ + -I${BOOST_INCLUDE_DIR} \ + -I $filePath \ + -c $file -o $workdir/built/$name) + + ($PRINT_CMDS; @WASM_LLVM_LINK@ -only-needed -o $workdir/linked.bc $workdir/built/* \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/eosiolib.bc \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc++.bc \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc.bc + ) + ($PRINT_CMDS; @WASM_LLC@ -thread-model=single --asm-verbose=false -o \ + $workdir/assembly.s $workdir/linked.bc) + ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-s2wasm -o $outname -s \ + 16384 $workdir/assembly.s) + ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-wast2wasm $outname \ + ${outname%.*}.wasm -n) +} +``` +由上述的代码可知,智能合约的编译主要过程如下: +* 利用clang以wasm32为目标,生成中间文件bc +* 利用LLVM-link链接上一个步骤生成bc文件和标准库bc文件生成link.bc文件 +* 利用LLVM的llc生成s汇编文件assembly.s +* 应用eosio-s2wasm工具讲s文件转化为wast文件 +* 应用eosio-wast2wasm工具将wast文件转化为最终的wast文件 + +通过以上的步骤我们就生成了一个以wasm为格式的智能合约,上面一共经历了5个步骤才将我们的
+源文件变异成wasm,其实还可以应用开源工具emcc来编译,但是该工具并不是针对智能合约设计
+工具比较庞大,我们把没有应用emcc的wasm的生成方案统一称为wasm without emcc。
+由于上述的编译过程很复杂,这里需要分析说明一下为什么采用这种方式?
+>The Runtime is the primary consumer of the byte code. It provides an API for
+instantiating WebAssembly modules and calling functions exported from them.
+To instantiate a module, it initializes the module's runtime environment
+(globals, memory objects, and table objects), translates the byte code into LLVM
+IR, and uses LLVM to generate machine code for the module's functions. + +由上文我们得知,WAVM是将wasm或者wast文件转化为LLVM的IR表示,然后通过LLVM运行代码来实现
+最后的程序运行,那么问题来了,***对于智能合约,为什么我们不直接用clang生成bc文件,然后修改
+lli(前文介绍过代码不超过800行)来实现虚拟机呢?*** 个人分析主要有以下几个原因: +* 如果EOS定义智能合约二进制格式为bc,文本方式为ll,也就是对标wasm和wast个人觉得利用lli
+没有问题,关键受限于LLVM。 +* 处于对未来的考虑,毕竟对wasm支持的解释容器比较多,方便多种虚拟机的接入,但是目前看大多数
+容器都是浏览器js引擎,因此解决js胶水代码仍然是个问题,所以寻求一个wasm的虚拟机目前看WAVM
+比较合适 +* WAVM实现了wasm的虚拟机,而且EOS也声称不提供虚拟机,也就是说wasm的选型限制了以上的工具链
+ +这里还有个重要的文件生成,那就是abi的文件的构建,这个的实现也在eosiocpp.in中,abi这里的
+作用是什么?就是它会描述一个合约对外暴露的接口,具体为JSON格式,用户可以通过eosc工具构建
+合适的message来调用对应的接口。eosiocpp中generate_abi的部分代码如下: +``` + ${ABIGEN} -extra-arg=-c -extra-arg=--std=c++14 -extra-arg=--target=wasm32 \ + -extra-arg=-nostdinc -extra-arg=-nostdinc++ -extra-arg=-DABIGEN \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \ + -extra-arg=-I${BOOST_INCLUDE_DIR} \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include -extra-arg=-I$context_folder \ + -extra-arg=-fparse-all-comments -destination-file=${outname} -verbose=0 \ + -context=$context_folder $1 --}} +``` +最后通过二进制工具cleos来部署智能合约,例如: +```c++ + cleos set contract eosio build/contracts/eosio.bios -j -p eosio +``` +* 第一个eosio为账户 +* 第二个eosio为权限 +* -j 以json输出结果 +* build/contracts/eosio.bios 为智能合约所在目录 + +同时可以通过cleos工具来推送action测试contract,例如如下命令: +```c++ +cleos push action eosio.token create '{"issuer":"eosio", "maximum_supply":" +1000000000.0000 EOS", "can_freeze":0, "can_recall":0, "can_whitelist":0}' +-j -p eosio.token +``` +* esoio.token为contract +* create为action +* 后面json格式的为具体的数据 +* -p为指定权限 + +小结:EOS智能合约通过复杂的工具链最后生成wasm或者wast,并配合abi文件最后进行分发到EOS
+系统中去。 + +## EOS虚拟机分析 +EOS在技术白皮书中指明并不提供具体的虚拟机实现,任何满足沙盒机制的虚拟机都可以运行在EOSIO
+中,源代码层面,EOS提供了一种虚拟机的实现,虚拟机以wasm为输入,利用相关的技术完成代码的
+快速执行。 +### EOS虚拟机概览 +EOS虚拟机代码实现来自WAVM,参见具体的文件发现其基本上都是wasm-jit目录下的内容从项目信
+息可以看出其是fork AndrewScheidecker/WAVM的实现,这个也是为啥很多人瞧不起EOS虚拟机的
+原因,但是Andrew Scheidecker本人主要在提交代码,所以对他下结论为时尚早,作者
+Andrew Scheidecker是虚幻引擎的主要贡献者,代码质量至少能有所保障。
+首先是EOS虚拟机的代码,在github上有两个地方可以查看到EOS中虚拟机的代码分别为:
+* https://github.com/EOSIO/eos +* https://github.com/EOSIO/WAVM + +其中eos目录下为这个EOS的代码其中虚拟机部分主要存在于如下几个关键的目录下: +* libraries/chain,主要是定义虚拟机相关接口 +* libraries/wasm-jit,主要是智能合约执行的实现 +* contracts目录下,为相关的ABI辅助源代码 + +>This is a standalone VM for WebAssembly. It can load both the standard binary
+format, and the text format defined by the WebAssembly reference interpreter.
+For the text format, it can load both the standard stack machine syntax and
+the old-fashioned AST syntax used by the reference interpreter, and all of the
+testing commands + +由上述的描述我们可以知道WAVM支持两种的输入分别是二进制的输入和文本格式的输入,对应的具
+体的格式是wasm和wast。参见WAVM使用说明如下: +>The primary executable is wavm:
+Usage: wavm [switches] [programfile] [--] [arguments]
+ in.wast|in.wasm Specify program file (.wast/.wasm)
+ -f|--function name Specify function name to run in module rather than main
+ -c|--check Exit after checking that the program is valid
+ -d|--debug Write additional debug information to stdout
+ -- Stop parsing arguments
+ +由上我们得知EOS的智能合约支持两种格式分别就是上文描述的wasm和wast。
+### EOS虚拟机实现思路分析 +EOS在智能合约目标格式选择上应该做过一定的考虑,对于wasm的选择可能出于社区支持和实现上
+的双重考虑,这点在采用LLVM-JIT技术就有所体现。EOS在选择如何实现虚拟机的方案上采用的是
+开放的态度,即如白皮书所讲。EOS为了使项目完整,需要提供一个的虚拟机。首先选定wasm不仅
+仅是因为支持的大厂比较多,还有出于多语言支持的考虑,敲定wasm目标格式后痛苦的事情就来了
+目前需要一个能执行他的虚拟机容器,目前都是浏览器支持,落地就是JS的解析引起支持,如果用JS
+解析引擎,工程量大,发布还要附带js胶水代码加麻烦的还有结果如何安全获取。于是需要的是一个
+wasm的执行的轻量级虚拟机,WAVM成了首选,多亏AndrewScheidecker之前写过一个这样的项
+目,于是直接Fork,加些接口就完成了implementation。***从另外一个角度看*** 如果不考虑生态的问题,
+LLVM中的bc也可以作为智能合约的语言,通过修改lli来完成虚拟机的实现,而且工程实践更加简单,
+但是问题就是和LLVM绑定了,虚拟机只能和LLVM混,这个限制太大。 + +### EOS虚拟机架构概述 +EOS虚拟机面对的编译的智能合约格式为wasm或者wast两种格式,这两种格式本质上没有区别,那么
+如何解析这两种格式并执行内部的相关指令就称为虚拟机主要考虑的问题,EOS的实现思路如下: +* 将wasm转化为LLVM能识别的IR中间语言。 +* 借助LLVM-JIT技术来实现IR的语言的运行。 + +这里有两个关键点,一个是如何将wasm格式文件转化为IR中间文件,第二个就是如何保证IR的相关
+运行时环境的维护。以下几个章节将解释相关的问题。 + +### EOS虚拟机实现与分析 +#### EOS虚拟机核心接口 +我们先High Level的看一下EOS虚拟机是如何响应外部执行需求的,这个主要体现在对外接
+层面EOS虚拟机接口对外暴露虚拟机实例创建和合约执行入口,具体声明在如下路径文件中
+***libraries/chain/inlcude/eosio/chain/webassembly/runtime_interface.hpp***
+文件中主要对外暴露了两个接口,分别为instantiate_module和apply,分别声明在两个不同
+的类中,如下为接口的具体声明:
+```c++ +class wasm_instantiated_module_interface { +public: + virtual void apply(apply_context& context) = 0; + virtual ~wasm_instantiated_module_interface(); +}; +class wasm_runtime_interface { +public: + virtual std::unique_ptr + instantiate_module(const char* code_bytes, + size_t code_size, + std::vector initial_memory) = 0; + virtual ~wasm_runtime_interface(); +}; +``` +接口apply实现在文件\libraries\chain\include\eosio\chain\webassembly\wavm.hpp中
+接口instantiate_module实现在\libraries\chain\webassembly\wavm.hpp中
+接口**apply**的实现如下代码所示:
+```c++ +void apply(apply_context& context) override { + //组织参数列表 + //这里需要说明一下每个被分发的action通过scope就是account和 + //function就是name来定义的 + vector args = { + Value(uint64_t(context.receiver)),//当前运行的代码 + Value(uint64_t(context.act.account)),//action中的账户 + Value(uint64_t(context.act.name))};//action的名称 + call("apply", args, context); +} +``` +下面来看call具体执行的逻辑功能,这里我们将看到运行在虚拟机上的代码是如何启动的。
这里我们*一行一行*来进行分析:
+```c++ +void call( + const string &entry_point, //函数入口点,例如:main是一个exe的入口 + const vector &args, //函数参数列表 + apply_context &context) {//需要执行的具体的内容 + try { + //首先根据entry_point(这里为apply)获取到传入的代码中是否有名字为 + //entry_point的object,通俗的讲就是根据函数名找到函数指针 + FunctionInstance* call = asFunctionNullable( + getInstanceExport(_ instance,entry_point)); + if( !call )//如果没有找到函数的入口在直接返回,注意此处无异常 + return; + //检查传入的参数个数和函数需要的个数是否相等,注意为什么没有检查类型 + //因为由上述函数apply得知类型均为uint_64,内部对应类型IR::ValuType::i64 + FC_ASSERT( getFunctionType(call)->parameters.size() == args.size() ); + + //获得内存实例,在一个wavm_instantiated_modules中,内存实例是被重用的, + //但是在wasm的实例中将不会看到getDefaultMemeory() + MemoryInstance* default_mem = getDefaultMemory(_ instance); + if(default_mem) { + //重置memory的大小为初始化的大小,然后清零内存 + resetMemory(default_mem, _ module->memories.defs[0].type); + char* memstart = &memoryRef(getDefaultMemory(_ instance), 0); + memcpy(memstart, _ initial_memory.data(), _ initial_memory.size()); + } + //设置运行上下文的内存和执行的上下文信息 + the_running_instance_context.memory = default_mem; + the_running_instance_context.apply_ctx = &context; + //重置全局变量 + resetGlobalInstances(_ instance); + //调用module的起始函数,这个函数做一些环境的初始化工作 + //其在instantiateModule函数中被设置 + runInstanceStartFunc(_ instance); + //invoke call(上面已经指向apply函数的地址了) + Runtime::invokeFunction(call,args); + } catch( const wasm_exit& e ) { + } catch( const Runtime::Exception& e ) { + FC_THROW_EXCEPTION(wasm_execution_error,"cause: ${cause}\n${callstack}", + ("cause", string(describeExceptionCause(e.cause))) + ("callstack", e.callStack)); + } FC_CAPTURE_AND_RETHROW() +} +``` +上述代码中通过call寻找entry_point名字的函数,这里为apply,注意上一个主题中EOSIO_ABI
+的展开中为apply函数的实现,如下: +```c++ +#define EOSIO_ABI( TYPE, MEMBERS ) \ +extern "C" { \ + void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \ + auto self = receiver; \ + if( action == N(onerror)) { \ + eosio_assert(code == N(eosio), \ + +``` +**总结**:上面通过接口的了解和代码的阅读分析快速的从比较高的视角看到EOS虚拟机执行
+的大体过程,下面我们就从细节上来了解EOS虚拟的采用的技术和最后是如何应用在EOS系统中的。 +#### EOS虚拟机架构应用层 +我们这里将EOS虚拟机关于智能合约部署以及虚拟机外层调用逻辑统一称为虚拟机应用层,现在分别进行
+说明,先从虚拟机的外围了解其整体的工作流程。 +##### EOS虚拟机客户端合约部署 +首先看命令行工具***cleos是如何将智能合约发送给EOSIO程序***的,具体的代码见文件:
+eosio\eos\programs\cleos\main.cpp。如下代码片段为添加命令行参数。
+```c++ +auto contractSubcommand = setSubcommand->add_subcommand( + "contract", + localized("Create or update the contract on an account")); +contractSubcommand->add_option( + "account", + account, + localized("The account to publish a contract for")) + ->required(); +contractSubcommand->add_option( + "contract-dir", + contractPath, + localized("The path containing the .wast and .abi")) + ->required(); +contractSubcommand->add_option( + "wast-file", + wastPath, + localized("The file containing the contract WAST or WASM relative to contract-dir")); +auto abi = contractSubcommand->add_option( + "abi-file,-a,--abi", + abiPath, + localized("The ABI for the contract relative to contract-dir")); +``` +上述的命令为set命令的子命令,现在看一下命令是如何发送出去的,主要在如下两个回调函数
+* set_code_callback +* set_abi_callback + +两个回调函数,我们以set_code_callback分析是如何运行的,关键代码如下: +```c++ +actions.emplace_back( create_setcode(account, bytes(wasm.begin(), wasm.end()) ) ); +if ( shouldSend ) { + std::cout << localized("Setting Code...") << std::endl; + send_actions(std::move(actions), 10000, packed_transaction::zlib); +} +``` +如上代码知道其是调用send_actions将智能合约的相关信息已一个action的形式发送出去,
+而send_actions将调用push_action函数,最后push_action将调用关键函数call代码如下:
+```c++ +fc::variant call( const std::string& url, + const std::string& path, + const T& v ) { +try { + eosio::client::http::connection_param * cp = + new eosio::client::http::connection_param((std::string&)url, (std::string&)path, + no_verify ? false : true, headers); + return eosio::client::http::do_http_call( *cp, fc::variant(v) ); +} +} +``` +由上可知客户端最后通过http的方式将部署智能合约的代码发送到了EOSIO上。注意其中的url具体为
+```c++ +const string chain_func_base = "/v1/chain"; +const string push_txn_func = chain_func_base + "/push_transaction"; +``` +##### EOS虚拟机服务端合约部署 +上面我们了解了合约是如何通过客户端传递到服务端的,现在我们重点分析一下服务端是如何部署
+或者更加准确的说存储合约的。我们重点分析一下nodeos(eosio\eos\programs\nodeos)是如何
+处理push_transaction的,先看其主函数关键片段: +```c++ +if(!app().initialize + (argc, argv)) +return INITIALIZE_FAIL; +initialize_logging(); +ilog("nodeos version ${ver}", \ + ("ver", eosio::utilities::common::itoh(static_cast(app().version())))); +ilog("eosio root is ${root}", ("root", root.string())); +app().startup(); +app().exec(); +``` +主要注册了chain,http,net和producer几个插件,我们先看chain_api_plugin的关键实现: +```c++ +auto ro_api = app().get_plugin().get_read_only_api(); +auto rw_api = app().get_plugin().get_read_write_api(); + +app().get_plugin().add_api( +CHAIN_RO_CALL(get_info, 200l), +CHAIN_RO_CALL(get_block, 200), +CHAIN_RO_CALL(get_account, 200), +CHAIN_RO_CALL(get_code, 200), +CHAIN_RO_CALL(get_table_rows, 200), +CHAIN_RO_CALL(get_currency_balance, 200), +CHAIN_RO_CALL(get_currency_stats, 200), +CHAIN_RO_CALL(get_producers, 200), +CHAIN_RO_CALL(abi_json_to_bin, 200), +CHAIN_RO_CALL(abi_bin_to_json, 200), +CHAIN_RO_CALL(get_required_keys, 200), +CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202), +CHAIN_RW_CALL_ASYNC(push_transaction, \ + chain_apis::read_write::push_transaction_results, 202), +CHAIN_RW_CALL_ASYNC(push_transactions, \ + chain_apis::read_write::push_transactions_results, 202) \ +) +``` +我们下一步具体详细的看一下http_plugin中的add_api的具体实现代码如下: +```c++ +void add_api(const api_description& api) { + for (const auto& call : api) + add_handler(call.first, call.second); +} +void http_plugin::add_handler(const string& url, const url_handler& handler) { + ilog( "add api url: ${c}", ("c",url) ); + //注册api函数,可以参看asio的pos示例 + app().get_io_service().post([=](){ + my->url_handlers.insert(std::make_pair(url,handler)); +}); +} +``` +由上面的函数我们得知,对url(例如/push_transactions)的请求通过注册的机制放入asio中。
+我们来看一下处理http请求的函数的关键代码,片段如下: +```c++ +auto handler_itr = url_handlers.find( resource ); +if( handler_itr != url_handlers.end()) { +con->defer_http_response(); + //这里将数据传递给了api相关的函数 + handler_itr->second( resource, body, [con]( auto code, auto&& body ) { + con->set_body( std::move( body )); + con->set_status( websocketpp::http::status_code::value( code )); + con->send_http_response(); +} ); +} +``` +**小结** 由上面的代码分析,我们基本清楚了一个请求过来是如何关联到具体的api函数的,下面
+我们来看一下如何实现合约的部署。 +我们先回到如下的代码片段,看具体处理函数是如何运行的
+```c++ +CHAIN_RW_CALL_ASYNC(push_transaction, \ + chain_apis::read_write::push_transaction_results, 202), + +``` +将对应的宏进行展开如下: +```c++ +#define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code) \ + CALL_ASYNC(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code) +``` +继续进行展开为如下的lamda表达式函数。 +```c++ +#define CALL_ASYNC(api_name, api_handle, api_namespace, call_name, call_result, \ + http_response_code) \ +{std::string("/v1/" #api_name "/" #call_name), \ + [this, api_handle](string, string body, url_response_callback cb) \ + mutable { \ + if (body.empty()) body = "{}"; \ + api_handle.call_name(\ + fc::json::from_string(body).as(),\ + [cb, body](const fc::static_variant& result){\ + if (result.contains()) {\ + try {\ + result.get()->dynamic_rethrow_exception();\ + } catch (...) {\ + http_plugin::handle_exception(#api_name, #call_name, body, cb);\ + }\ + } else {\ + cb(http_response_code, result.visit(async_result_visitor()));\ + }\ + });\ + }\ +} +``` +由上述的关键代码,我们对应得到具体处理的函数cb为函数rw_api,下面我们来看一下chain_pulgin
+下的rw_api的具体实现,由如下的代码片段我们得知关键处理的类为read_write+ +```c++ +chain_apis::read_write chain_plugin::get_read_write_api() { + return chain_apis::read_write(chain()); +} +``` +我们现在看一下类chain_apis::read_write中push_transaction的具体实现, +```c++ +void read_write::push_transaction(const read_write::push_transaction_params& params, + next_function next) { + try { + ............. + ............. + //关键处理在get_method方法所获得的具体的处理函数 + app().get_method()( + pretty_input,//输入数据 + true, + [this, next](const fc::static_variant& result) -> void{ + if (result.contains()) { + //执行函数next + next(result.get()); + } else { + auto trx_trace_ptr = result.get(); + try { + fc::variant pretty_output; + pretty_output = db.to_variant_with_abi(*trx_trace_ptr); + chain::transaction_id_type id = trx_trace_ptr->id; + //执行next函数 + next(read_write::push_transaction_results{id, pretty_output}); + } CATCH_AND_CALL(next); + } + }); +}} +``` +我们来看一下`incoming::methods::transaction_async`对应的具体的处理函数: +```c++ +namespace methods { + // synchronously push a block/trx to a single provider + using block_sync = method_decl; + using transaction_async = method_decl), + first_provider_policy>; +} +``` +这里重点关注method_decl(声明在libraries/appbase/include/appbase/method.h)下。 +其原型为: +```c++ +//@tparam Tag - API specific discriminator used to distinguish between otherwise +// identical method signatures +//@tparam FunctionSig - the signature of the method +//@tparam DispatchPolicy - dispatch policy that dictates how providers +//for a method are accessed defaults to @ref first_success_policy +template< typename Tag, typename FunctionSig, + template class DispatchPolicy = first_success_policy> +struct method_decl { + using method_type = method>; + using tag_type = Tag; +}; +``` +最后我们回到开始的`get_method`来看下其具体做了
+什么,参看文件为eos/libraries/appbase/include/appbase/application.hpp +```c++ +//fetch a reference to the method declared by the passed in type. This will +//construct the method on first access. This allows loose and deferred +//binding between plugins +//@tparam MethodDecl - @ref appbase::method_decl +//@return reference to the method described by the declaration + +template +auto get_method() -> +std::enable_if_t::value, typename MethodDecl::method_type&> +{ + //我们展开后得到method_type的类型为incoming::methods::method_decl:: + //method), + //first_provider_policy>, 这个类型看似比较复杂,但是抓住关键就是函数签名 + //void(const packed_transaction_ptr&, bool , next_function) + // + using method_type = typename MethodDecl::method_type; + auto key = std::type_index(typeid(MethodDecl)); + auto itr = methods.find(key); + if (itr != methods.end()) { + //这里我们得到了具体的函数,那么下一步就是看函数如何运行的。 + return * method_type::get_method(itr->second); + } else { + methods.emplace(std::make_pair(key, method_type::make_unique())); + return * method_type::get_method(methods.at(key)); + } +} +``` +通过上面的代码我们可以得出结论关键的transaction处理函数原型如下:
+`void(const packed_transaction_ptr&, bool , next_function)`
+按图索骥,于是我们找到了producer_plugin.cpp的实现,在函数plugin_initialize的结尾处我们
+看到如下的代码: +```c++ +my->_incoming_transaction_async_provider = app(). + get_method().register_provider([this]( + //注意此处的函数的签名 + const packed_transaction_ptr& trx, + bool persist_until_expired, + next_function next) -> void { + return my->on_incoming_transaction_async(trx, persist_until_expired, next ); +}); +``` +这里是将具体处理的函数进行注册到具体的关联的type上,那么下一步我们就着重分析函数:
+`on_incoming_transaction_async`就可以了。其实现在文件producer_plugin.cpp中。其中
+关键函数代码为: +```c++ +auto send_response = [this, &trx, &next](const fc::static_variant& response) { + next(response); + if (response.contains()) { + _ transaction_ack_channel.publish(std::pair + (response.get(), trx)); + } else { + //将数据发入到channel中,具体的订阅者将会进行处理。 + _ transaction_ack_channel.publish(std::pair + (nullptr, trx)); + } +}; +``` +上面的transaction_ack_channel由net_plugin进行订阅,这点我们可以理解,主要是发送ack返回消息。
+进一步在函数on_incoming_transaction_async中如下代码对transaction进行了处理: +```c++ +//调用chain的push_transaction来处理transaction, +auto trace = chain.push_transaction(std::make_shared(*trx), deadline); +if (trace->except) { + if (failure_is_subjective(*trace->except, deadline_is_subjective)) { + _ pending_incoming_transactions.emplace_back(trx, persist_until_expired, next); + } else { + auto e_ptr = trace->except->dynamic_copy_exception(); + send_response(e_ptr); + } +} +``` +跟踪代码最后我们知道chain的类型为eosio::chain::controller,具体见文件:
+eos\libraries\chain\include\eosio\chain\controller.h,代码如下: +```c++ +transaction_trace_ptr controller::push_transaction(const transaction_metadata_ptr& trx, + fc::time_point deadline, + uint32_t billed_cpu_time_us ) { + //其中my的类型为controller_impl, + return my->push_transaction(trx, deadline, false, billed_cpu_time_us); +} +``` +最后我们看一下controller_impl中的具体是如何实现push_transaction的,关键的代码如下,注意
+其中的具体的注释: +```c++ +transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx, + fc::time_point deadline, + bool implicit, + uint32_t billed_cpu_time_us) +{ + try { + //首先生成transaction的context上下文, + transaction_context trx_context(self, trx->trx, trx->id); + trx_context.deadline = deadline; + trx_context.billed_cpu_time_us = billed_cpu_time_us; + trace = trx_context.trace; + try { + ...... + //检查actor是否有在黑名单中的 + if (trx_context.can_subjectively_fail && + pending->_block_status == controller::block_status::incomplete ) { + check_actor_list( trx_context.bill_to_accounts ); + } + //进行权限检查 + trx_context.delay = fc::seconds(trx->trx.delay_sec); + if(!self.skip_auth_check() && !implicit ) { + authorization.check_authorization( + trx->trx.actions, + trx->recover_keys( chain_id ), + {}, + trx_context.delay, + [](){} + false + ); + } + //执行transaction 这里是关键的步骤,这里将涉及到具体的transaction是如何继续 + //往下走的 + trx_context.exec(); + trx_context.finalize(); + emit(self.applied_transaction, trace); + + trx_context.squash(); + restore.cancel(); + ..... + //此处省略和本次介绍无关的代码 + } catch (const fc::exception& e) { + trace->except = e; + trace->except_ptr = std::current_exception(); + } + return trace; + } FC_CAPTURE_AND_RETHROW((trace)) +} +``` +我们先重点看下transaction_context的exec函数是如何实现的,关键代码片段如下,从中我们可以
+看到action的相关延迟处理逻辑,以及对是否是上下文无关的处理。 +```c++ +if( apply_context_free ) { + for( const auto& act : trx.context_free_actions ) { + trace->action_traces.emplace_back(); + dispatch_action( trace->action_traces.back(), act, true ); + } +} +//有延迟的函数被认定为上下文相关 +if( delay == fc::microseconds() ) { + for( const auto& act : trx.actions ) { + trace->action_traces.emplace_back(); + dispatch_action( trace->action_traces.back(), act ); + } +} else { + schedule_transaction(); +} +``` +我们看下dispacth是如何进行分发action的,这里需要注意,我们上一步关注的实体还是transaction,
+这里已经细化到action,后面将看到具体是如何处理的,例如inline action的处理。 +```c++ +void transaction_context::dispatch_action( + action_trace& trace,//trance跟踪 + const action& a, //传入的action + account_name receiver, //account 其值为a.account + bool context_free, //是否上下文无关 + uint32_t recurse_depth ) { //递归的层数,这里用于inline action的递归调用 + //生成apply_context对象 + apply_context acontext( control, * this, a, recurse_depth ); + acontext.context_free = context_free; + acontext.receiver = receiver;//这里设置了账户,来自action + try { + acontext.exec();//关键函数,执行apply操作 + } catch( ... ) { + trace = move(acontext.trace); + throw; + } + trace = move(acontext.trace); +} +``` +我们来看一下apply_context的exec的具体执行流程,如下代码所示。我们可以看到其中递归的调用
+但是在递归调用之前调用了exec_one函数,这个最终的关键:
+```c++ +_notified.push_back(receiver); +trace = exec_one();//关键函数 +.......//此处省略非重要代码 +for ( const auto& inline_action : _ cfa_inline_actions ) { + trace.inline_traces.emplace_back(); + trx_context.dispatch_action( trace.inline_traces.back(), + inline_action, + inline_action.account, + true, recurse_depth + 1 ); +} +//还是上下文无关的分开处理 +for ( const auto& inline_action : _ inline_actions ) { + trace.inline_traces.emplace_back(); + trx_context.dispatch_action( trace.inline_traces.back(), + inline_action, + inline_action.account, + false, recurse_depth + 1 ); +} +``` +我们继续展开最后的函数exec_one函数,其涉及到智能合约的关键代码片段如下: +```c++ +const auto &a = control.get_account(receiver); + privileged = a.privileged; + //这里有两种不同的过程要进行处理,分别是native的和传入的, + //更加准确的是系统的和智能合约的两种不同的形式 + auto native = control.find_apply_handler(receiver, act.account, act.name); + if( native ) { + if( trx_context.can_subjectively_fail && control.is_producing_block() ) { + control.check_contract_list( receiver ); + control.check_action_list( act.account, act.name ); + } + (* native)(* this); + } + //如果说其code大于0,并且账户费系统并且非setcode则执行 + if( a.code.size() > 0 && + !(act.account == config::system_account_name && + act.name == N(setcode) && receiver == config::system_account_name) ) + { + if( trx_context.can_subjectively_fail && control.is_producing_block() ) { + control.check_contract_list( receiver ); + control.check_action_list( act.account, act.name ); + } + try { + //最后执行的apply函数 + //调用具体的apply函数进行执行 + control.get_wasm_interface().apply(a.code_version, a.code, *this); + } catch ( const wasm_exit& ){} + } +``` +这里我们先分析**find_apply_handler**的过程,首先我们先找到具体的handler注册的机制
+具体见文件eosio\eos\libraries\chain\controller.cpp +```c++ +void set_apply_handler( account_name receiver, account_name contract, + action_name action, apply_handler v ) { + //具体实现为一个map数据结构 + apply_handlers[receiver][make_pair(contract,action)] = v; +} +在controller_impl的初始化函数中,我们看到如下代码片段,到此我们看到了我们的set contract
+实际上是调用了系统的一个预设的合约或者说是函数。 +```c++ +#define SET_APP_HANDLER( receiver, contract, action) \ + set_apply_handler( #receiver, #contract, #action, \ + &BOOST_PP_CAT(apply_, BOOST_PP_CAT(contract, BOOST_PP_CAT(_ ,action) ) ) ) + + SET_APP_HANDLER( eosio, eosio, newaccount ); + SET_APP_HANDLER( eosio, eosio, setcode ); + SET_APP_HANDLER( eosio, eosio, setabi ); + SET_APP_HANDLER( eosio, eosio, updateauth ); + SET_APP_HANDLER( eosio, eosio, deleteauth ); + SET_APP_HANDLER( eosio, eosio, linkauth ); + SET_APP_HANDLER( eosio, eosio, unlinkauth ); +``` +这里我们看apply_contract_action的具体实现,代码在eosio\eos\libraries\chain\eosio_contract.cpp
+中,这个文件定义了系统的contract的具体实现,关键代码片段如下: +```c++ + +auto& db = context.db; +//这里set_code的具体格式如下 +//struct setcode { +// account_name account; +// uint8_t vmtype = 0; +// uint8_t vmversion = 0; +// bytes code; +//} +auto act = context.act.data_as(); +context.require_authorization(act.account); +fc::sha256 code_id; /// default ID == 0 +if( act.code.size() > 0 ) { + //计算具体的code_id + code_id = fc::sha256::hash( act.code.data(), (uint32_t)act.code.size() ); + wasm_interface::validate(context.control, act.code); +} +const auto& account = db.get(act.account); +int64_t code_size = (int64_t)act.code.size(); +int64_t old_size = (int64_t)account.code.size() * config::setcode_ram_bytes_multiplier; +int64_t new_size = code_size * config::setcode_ram_bytes_multiplier; +//检查前后的code的版本 +FC_ASSERT( account.code_version != code_id, + "contract is already running this version of code" ); +//将code更新到db中 +db.modify( account, [&]( auto& a ) { + a.last_code_update = context.control.pending_block_time(); + a.code_version = code_id; + a.code.resize( code_size ); + if( code_size > 0 ) + memcpy(a.code.data(), act.code.data(), code_size ); + + }); +} +``` +**小结** 最后我们看到了我们的合约代码被更新到对应的account中去,也就是智能合约账户中去。
+##### EOS虚拟机服务端合约的调用执行 +在**EOS虚拟机核心接口**一章中我们了解到调用虚拟机执行智能合约的接口函数为apply,通过上面
+的分析我们得知在执行action的时候我们发现在exec_one中有如下代码片段: +```c++ +try { + //最后执行的apply函数 + //调用具体的apply函数进行执行 + control.get_wasm_interface().apply(a.code_version, a.code, *this); +} catch ( const wasm_exit& ){} +``` +这样我们就能整体的把握了具体的流程,代替顺序如下: +* transaction分发到nodeos +* nodeos验证transaction然后进行执行 +* 由于transaction是由action组成的,所以最终落到具体的action上 +* 在执行(exec_one)中调用apply的接口将具体的合约传递到虚拟机去执行 +我们现在看一下wasm_interface的apply的实现: +```c++ +void wasm_interface::apply( const digest_type& code_id, + const shared_string& code, + apply_context& context ) { + my->get_instantiated_module(code_id, code, context.trx_context)->apply(context); +} +``` +其中get_instantiated_module的实现在wasm_interface_private.hpp中,具体如下: +```c++ +auto it = instantiation_cache.find(code_id); +//如果内部没有该智能合约的缓存则进行创建 +if(it == instantiation_cache.end()) { + auto timer_pause = fc::make_scoped_exit([&](){ + trx_context.resume_billing_timer(); + }); + trx_context.pause_billing_timer(); + IR::Module module; + try { + //加载wasm二进制序列化对象 + Serialization::MemoryInputStream stream((const U8*)code.data(), code.size()); + WASM::serialize(stream, module); + module.userSections.clear(); + } catch(const Serialization::FatalSerializationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } catch(const IR::ValidationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } + //执行相关的注入代码 + wasm_injections::wasm_binary_injection injector(module); + injector.inject(); + + std::vector bytes; + try { + Serialization::ArrayOutputStream outstream; + WASM::serialize(outstream, module); + bytes = outstream.getBytes(); + } catch(const Serialization::FatalSerializationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } catch(const IR::ValidationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } + //生成新的wasm_instantiated_module_interface对象插入到map中去。 + it = instantiation_cache.emplace(code_id, + runtime_interface->instantiate_module((const char*)bytes.data(), + bytes.size(), parse_initial_memory(module))).first; +} +return it->second; +} +``` +**小结** 通过以上代码我们得知最后我们获得的是一个wasm_instantiated_module_interface的对象,
+然后调用apply函数来实现最后的作用,至此虚拟机应用层分析告一段落。上面代码具体涉及到的
+WASM-JIT范畴的内容,下一章将继续详细介绍。 + +#### EOS虚拟机Module IR生成 +由上文得知函数get_instantiated_module会从传入的二进制字节码生成一个Module实例,下面我们就
+具体分析一下其是如何进行解析生成的。现分析get_instantiated_module(wasm_interface_private.hpp)
+中。首先看如下代码片段: +```c++ +IR::Module module; +try { + Serialization::MemoryInputStream stream((const U8*)code.data(), code.size()); + WASM::serialize(stream, module); //该步骤完成从stream到module的转化 + module.userSections.clear(); + } +``` +函数WASM::serialize的函数原型如下,这里先生成中间语言IR下的Module,然后再进行校验。 +```c++ +void serialize(Serialization::InputStream& stream,Module& module) +{ + //函数serializeModule有两个重载,在于一个参数是InputStream + //而另外一个是OutputStream + serializeModule(stream,module); + IR::validateDefinitions(module); +} +``` +现在看关键函数serializeModule的实现, 以下的为关键代码片段。具体的参见文件:
+eos\libraries\wasm-jit\Source\WASM\WASMSerializatin.cpp +```c++ +//首先读取WASM文件头部的MagicNumber和版本号 +serializeConstant(moduleStream,"magic number",U32(magicNumber)); +serializeConstant(moduleStream,"version",U32(currentVersion)); +SectionType lastKnownSectionType = SectionType::unknown; +while(moduleStream.capacity()) +{ + const SectionType sectionType = *(SectionType*)moduleStream.peek(sizeof(SectionType)); + if(sectionType != SectionType::user) + { + //这里要求解析的的section的顺序需要和已知的顺序一致,具体的顺序可以参考 + //类型SectionType的定义 + if(sectionType > lastKnownSectionType) { lastKnownSectionType = sectionType; } + else { throw FatalSerializationException("incorrect order for known section"); } + } + switch(sectionType) + { + //如果解析的字节对应的类型是type,那么调用反序列化接口 + case SectionType::type: serializeTypeSection(moduleStream,module); break; + case SectionType::import: serializeImportSection(moduleStream,module); break; + ........ + case SectionType::user: + {2 + UserSection& userSection = * module.userSections.insert( + module.userSections.end(),UserSection()); + serialize(moduleStream,userSection); + break; + } + default: throw FatalSerializationException("unknown section ID"); + } + ; +} +``` +由上面函数的的代码片段我们得知处理的整体思路是按照已知的SectionType的类型依次向下进行进行
+这里我们举例分析serializeTypeSection,如下面的WASM的二进制格式我们得知,首先我们获得一个字节
+的type标识,然后一个字节是这个块的大小,目前为00,最后是这个块里面有多少个这样的type的描述。
+即其中num types对应的行。 +```c++ +0000000: 0061 736d ; WASM_BINARY_MAGIC +0000004: 0100 0000 ; WASM_BINARY_VERSION +; section "Type" (1) +0000008: 01 ; section code +0000009: 00 ; section size (guess) +000000a: 01 ; num types +; type 0 +000000b: 60 ; func +000000c: 00 ; num params +000000d: 01 ; num results +000000e: 7f ; i32 +0000009: 05 ; FIXUP section size +; section "Function" (3) +000000f: 03 ; section code +``` +我们现在看这里我们举例分析serializeTypeSection是如何处理的 +```c++ +serializeSection(moduleStream,SectionType::type,[&module](Stream& sectionStream) +{ + //函数serializeArray用来处理数组形式的type + serializeArray(sectionStream,module.types,[](Stream& stream, + const FunctionType*& functionType) + { + serializeConstant(stream,"function type tag",U8(0x60)); + if(Stream::isInput) + { + std::vector parameterTypes; + ResultType returnType; + serialize(stream,parameterTypes); + serialize(stream,returnType); + //根据参数列表和返回值列表生成函数的类型是 + functionType = FunctionType::get(returnType,parameterTypes); + } + else + { + serialize(stream,const_cast&>(functionType->parameters)); + serialize(stream,const_cast(functionType->ret)); + } + }); +}); +``` +这里用来lamda表达式我们先看serializeArray函数的函数原型,如下: +```c++ +template +void serializeArray(Stream& stream,std::vector& vector, + SerializeElement serializeElement) +{ +//此处省略非重要的代码 +for(Uptr index = 0;index < size;++index) +{ + vector.push_back(Element()); + //以下函数代码将调用匿名的lamda函数, + serializeElement(stream,vector.back()); +} +vector.shrink_to_fit() +} +``` +通过上述的代码我们得知serializeElement调用了匿名lamda函数,回到函数serializeSection中我们
+得知最后module.types将存储types的函数描述vector容器。
+**小结** 通过上面的代码描述我们知道从一段字节码最后转换为Module对象,Module对象对后续的执行
+有很大的帮助。
+我们来看一下Module对象中的具体数据结构。
+```c++ +struct Module +{ + std::vector types; + + IndexSpace functions; + IndexSpace tables; + IndexSpace memories; + IndexSpace globals; + + std::vector exports; + std::vector dataSegments; + std::vector tableSegments; + std::vector userSections; + + Uptr startFunctionIndex; + + Module() : startFunctionIndex(UINTPTR_MAX) {} +}; +``` +从上面的的代码我们得知一个Module的具体内部结构,但是我们还没有能进入IR层面。
+现在我们回到函数`void serialize(Serialization::InputStream& stream,Module& module)`
+在执行完成serializeModule后将执行`IR::validateDefinitions(module);`,我们来看下
+具体的关键实现,具体见文件\eos\libraries\wasm-jit\Source\IR\Validate.cpp
+```c++ +//检查FunctionType的参数 +for(Uptr typeIndex = 0;typeIndex < module.types.size();++typeIndex) +{ + const FunctionType* functionType = module.types[typeIndex]; + for(auto parameterType : functionType->parameters) { validate(parameterType); } + validate(functionType->ret); +} + +....... +//本处代码为依次检查function_import memory_import table_import global_import +//function_def global_def table_def memory_def 以及export的内容 +//一下的代码用来获取起始函数的函数类型 +//这里函数分支只有单独运行虚拟机通过loadTextModule才会使startFunctionIndex为有效值 +//我们这里不需要,因为入口函数就是apply +if(module.startFunctionIndex != UINTPTR_MAX) +{ + VALIDATE_INDEX(module.startFunctionIndex,module.functions.size()); + const FunctionType* startFunctionType = module.types[module.functions. + getType(module.startFunctionIndex).index]; + VALIDATE_UNLESS("start function must not have any parameters or + results: ",startFunctionType != FunctionType::get()); +} +//剩下为各种segment的检查 +``` +接下来执行如下部分代码 +```c++ +wasm_injections::wasm_binary_injection injector(module); +injector.inject(); +``` +主要向其中注入check_time函数代码,通过add_export函数来具体实现,这里就不描述。 +#### VirtualMachine实例化 +由上一个小节的介绍我们得知调用wasm_inteface的get_instantiated_module获得一个Module
+函数get_instantiated的最后代码会调用自身数据成员runtime_interface的initantiate_module
+函数来生成wasm_instantiated_module_interface的相关对象。在这里有两个类继承了
+wasm_instantiated_module_interface接口分别是: +* binaryen_instantiated_module +* wavm_instantiated_module + +如下为这部分的相关类的类图
+![RuntimeInterface](./image/RuntimeInterface.jpg "MainClass")
+由上图以及过程中的调用关系我们得到,首先我们确认runtime的类型是wavm还是binaryen,然后
+我们就能确认接口函数instatiate_module返回的wasm_instantiated_moudle_interface的
+具体类型是wavm_instantiated_module还是binayen_instantiated_module。
+ +在类controller中具体见文件\eos\libraries\chain\controller.cpp中的成员定义:
+```c++ +wasm_interface::vm_type wasm_runtime = chain::config::default_wasm_runtime; +``` +而在文件controller.hpp中:
+```c++ +const static eosio::chain::wasm_interface::vm_type default_wasm_runtime = +eosio::chain::wasm_interface::vm_type::binaryen; +``` +在controller_imp的构造函数中我们可以看到wasmif成员的初始化,如下所示 +```c++ +controller_impl( const controller::config& cfg, controller& s ) + :self(s), + db( cfg.state_dir, + cfg.read_only ? database::read_only : database::read_write, + cfg.state_size ), + reversible_blocks( cfg.blocks_dir/config::reversible_blocks_dir_name, + cfg.read_only ? database::read_only : database::read_write, + cfg.reversible_cache_size ), + blog( cfg.blocks_dir ), + fork_db( cfg.state_dir ), + wasmif( cfg.wasm_runtime ),//初始化wasm虚拟机的runtime + resource_limits( db ), +``` +因此我们得出结论,如果不是命令行指定虚拟机的种类,这里默认为binaryen类型。所以关键部分
+展开后就为binaryen_runtime的instantiate_module函数得到binaryen_instantiated_module
+对象,最后调用其apply的方法。这里需要关注文件eos\libraries\chain\webassembly下的文件
+* binaryen.cpp + * binaryen_instantiated_module的定义 + * wasm_instantiated_module_interface的apply接口实现 + * binaryen_runtime的instantiate_module方法实现 +* wavm.cpp + * wasm_instantiated_module_interface的apply接口实现 + * wasm_runtime接的instantiate_module方法实现 + +两种不同的解释器底层,在instantiated_module上不同毕竟一个用的是LLVM的JIT一个是用的
+是Binaryen的解释器。 + +#### Binaryen底层解释器 +##### ModuleInstance的创建 +首先我们看一下binaryen_runtime的instantiate_module方法是如何生成binaryed_instatiated_module
+的,具体相关代码如下: +```c++ +//首先创建WasmBinaryBuilder对象,这里需要注意下,类WasnBinaryBuilder的实现 +//在外部编译依赖Binaryen中,文件位于external/binaryen/src/wasm-binaryen.h中 +vector code(code_bytes, code_bytes + code_size); +//Module类型为Binaryen中的Module类型并非项目中IR的Module类型 +unique_ptr module(new Module()); +WasmBinaryBuilder builder(*module, code, false); +builder.read(); +//获取全局变量数值,并保存在global中 +TrivialGlobalManager globals; +for (auto& global : module->globals) { + globals[global->name] = ConstantExpressionRunner(globals). + visit(global->init).value; +} +//间接调用表 +call_indirect_table_type table; +table.resize(module->table.initial); +拷贝segment中的内容到间接调用表中去 +for (auto& segment : module->table.segments) { + Address offset = ConstantExpressionRunner(globals). + visit(segment.offset).value.geti32();//获得该段的大小 + FC_ASSERT( uint64_t(offset) + segment.data.size() <= module->table.initial); + for (size_t i = 0; i != segment.data.size(); ++i) { + table[offset + i] = segment.data[i]; + } +} +//获得import的相关函数,用map数据结构去维护 +import_lut_type import_lut; +import_lut.reserve(module->imports.size()); +for (auto& import : module->imports) { + std::string full_name = string(import->module.c_str()) + "." + string(import->base.c_str()); + if (import->kind == ExternalKind::Function) { + auto& intrinsic_map = intrinsic_registrator::get_map(); + auto intrinsic_itr = intrinsic_map.find(full_name); + if (intrinsic_itr != intrinsic_map.end()) { + import_lut.emplace(make_pair((uintptr_t)import.get(), intrinsic_itr->second)); + continue; + } + } +} +//最后返回具体的instiated_module +return std::make_unique(_ memory, initial_memory, +move(table), move(import_lut), move(module)) +} +``` +##### Appply接口的实现和调用 +由上面的代码我们得知binaryen的类型的instantiated_module需要的参数为内存,访问表
+(线性的)以及导入的对象列表。当我们拿到一个instantiated_module后,我们看一下是如何
+执行apply函数的。首先我们看一下函数apply的实现,会调用call函数,而从参数里面我们知道
+对于binaryen的相关内存的访问都是线性的。 +```c++ +void apply(apply_context& context) override { + LiteralList args = {Literal(uint64_t(context.receiver)), + Literal(uint64_t(context.act.account)), + Literal(uint64_t(context.act.name))}; + call("apply", args, context); +} +``` +下面我们详细的分析一下call函数具体执行了哪些操作 +```c++ +void call(const string& entry_point, LiteralList& args, apply_context& context){ + const unsigned initial_memory_size = _ module->memory.initial * Memory::kPageSize; + //声明一个解释器接口,传入的参数关键的为导入的对象map即_import_lut + interpreter_interface local_interface(_ shared_linear_memory, _ table, _ import_lut, + initial_memory_size, context); + //初始化内存和数据 + //zero out the initial pages + memset(_ shared_linear_memory.data, 0, initial_memory_size); + //copy back in the initial data + memcpy(_ shared_linear_memory.data, _ initial_memory.data(), _ initial_memory.size()); + + //生成module instance,这里的初始化会调用start function + ModuleInstance instance(* _ module.get(), &local_interface); + //调用具体执行的函数 + instance.callExport(Name(entry_point), args); +} +``` +如下两个类型需要详细的说明一下: +* interpreter_interface +* ModuleInstance + +首先是interpreter_insterface,其类型如下
+`struct interpreter_interface : ModuleInstance::ExternalInterface`
+位于文件\eos\libraries\chain\include\eosio\chain\webassembly\binaryen.hpp
+其中关键的函数为callImport和callTable,现在简要的说明一下: +```c++ +Literal callImport(Import * import, LiteralList& args) override +{ + //由于import_lut中存储的就是导入的函数或者对象的基本信息, + //则这里直接进行map的查找操作 + auto fn_iter = import_lut.find((uintptr_t)import); + EOS_ASSERT(fn_iter != import_lut.end(), wasm_execution_error,\ + "unknown import ${m}:${n}", ("m", import->module.c_str())("n",\ + import->module.c_str())); + return fn_iter->second(this, args); +} +``` +这里import_lut的类型为`unordered_map`
+我们可以看到具体的导入的函数描述类型为`intrinsic_registrator::intrinsic_fn>`这里我们
+看一下instrinsic_fn的具体类型: +```c++ +struct intrinsic_registrator { + using intrinsic_fn = Literal(*)(interpreter_interface*, LiteralList&); + ...... +} +} +``` +参数就是一个实现了ModuleInstance::ExternalInterface的类和参数列表 +下面我们看一下callTable的操作 +```c++ +Literal callTable(Index index, LiteralList& arguments, WasmType result, + ModuleInstance& instance) override +{ + EOS_ASSERT(index < table.size(), wasm_execution_error, "callIndirect: bad pointer"); + //根据函数表类似于ELF中的GOT来获取函数的指针 + auto* func = instance.wasm.getFunctionOrNull(table[index]); + EOS_ASSERT(func, wasm_execution_error, "callIndirect: uninitialized element"); + EOS_ASSERT(func->params.size() == arguments.size(), \ + wasm_execution_error, "callIndirect: bad # of arguments"); + //进行参数检查 + for (size_t i = 0; i < func->params.size(); i++) { + EOS_ASSERT(func->params[i] == arguments[i].type,\ + wasm_execution_error, "callIndirect: bad argument type"); + } + EOS_ASSERT(func->result == result, wasm_execution_error, "callIndirect: bad result type"); + //调用函数,这里的invoke机制就是最后程序执行的最根本依赖,下面将详细的分析一下 + return instance.callFunctionInternal(func->name, arguments); +} +``` +从上面我们可以看到interpreter_interface封装了函数的调用,无论是外部的还是内部自己实现的
+下面我们来看一下ModuleInstance的具体实现,文件位于如下的位置:
+eos\externals\binaryen\src\wasm-interpreter.h
+其他实现相关具体的需要看ModuleInstanceBase,在其中有很多load函数的实现,主要是加载对应
+的数据类型到内存中,现在我们看一下它的构造函数的实现: +```c++ +ModuleInstanceBase(Module& wasm, ExternalInterface* externalInterface) : + wasm(wasm), + externalInterface(externalInterface) { + // 导入外部全局的数据 + externalInterface->importGlobals(globals, wasm); + // 准备内存 + memorySize = wasm.memory.initial; + // 处理内部的全局数据 + for (auto& global : wasm.globals) { + globals[global->name] = ConstantExpressionRunner(globals). + visit(global->init).value; + } + //处理外部函数接口相关内容,这里就是上面讲到的interpter_inferface + externalInterface->init(wasm, *self()); + //运行函数starFunction + if (wasm.start.is()) { + LiteralList arguments; + callFunction(wasm.start, arguments); + } +} +``` +##### CallFunction的实现 +现在我们重点看一下callFunction是如何实现的,这样对于我们理解最外层的callExport函数有一定
+的帮助作用。下面我们来看下具体实现原理,如下为其代码片段,我们看到了久违的栈。 +```c++ +Literal callFunction(Name name, LiteralList& arguments) { + callDepth = 0; + functionStack.clear();//用到了栈 + return callFunctionInternal(name, arguments); +} +``` +现在看关键函数callFunctionInternal的实现,在函数的实现中实现了两个内部类,我们先看其主要
+的流程: +```c++ +Literal callFunctionInternal(Name name, LiteralList& arguments) { + if (callDepth > maxCallDepth) + externalInterface->trap("stack limit"); + auto previousCallDepth = callDepth; + callDepth++; + //保留之前函数的栈信息 + auto previousFunctionStackSize = functionStack.size(); + functionStack.push_back(name); //将函数名字入栈 + //获得函数指针,这里的函数指针所指向的内容后文将有所介绍 + Function* function = wasm.getFunction(name); + ASSERT_THROW(function); + //FunctinScope没有具体的实际操作。基本上都是参数检查和 + //返回值检查 + FunctionScope scope(function, arguments); + //这个类比较重要,这里涉及到了具体的执行流程控制. + RuntimeExpressionRunner rer(* this, scope); + Flow flow = rer.visit(function->body); + ASSERT_THROW(!flow.breaking() || flow.breakTo == RETURN_FLOW); + Literal ret = flow.value; //最后获得执行的结果 + if (function->result != ret.type) { + if (rer.last_call.value.type == function->result && ret.type == 0) { + ret = rer.last_call.value; + } + else { + std::cerr << "calling " << function->name << " resulted in " << ret + << " but the function type is " << function->result << '\n'; + WASM_UNREACHABLE(); + } + } + return ret; +} +``` +下面我们重点分析一下如下代码段,这段代码段控制了这个数据流程。 +```c++ +RuntimeExpressionRunner rer(* this, scope); +Flow flow = rer.visit(function->body); +``` +下面我们进入visit函数的实现,具体如下: +```c++ +return Visitor::visit(curr); +``` +最后我们进入Vistior的具体定义实现: +```c++ +struct Visitor { + // Expression visitors + ReturnType visitBlock(Block* curr) {} + ReturnType visitIf(If* curr) {} + ......... + // Module-level visitors + ReturnType visitFunctionType(FunctionType* curr) {} + ReturnType visitImport(Import* curr) {} + ReturnType visitExport(Export* curr) {} + ReturnType visitGlobal(Global* curr) {} + ReturnType visitFunction(Function* curr) {} + ReturnType visitTable(Table* curr) {} + ReturnType visitMemory(Memory* curr) {} + ReturnType visitModule(Module* curr) {} + ///从这段代码我们可以知道主要是SubType最后会影响 + ///我们创建的Visitior的类型,并影响调用的方法 + ReturnType visit(Expression* curr) { + ASSERT_THROW(curr); + #define DELEGATE(CLASS_TO_VISIT) \ + return static_cast(this)-> \ + visit##CLASS_TO_VISIT(static_cast(curr)) + + switch (curr->_id) { + case Expression::Id::BlockId: DELEGATE(Block); + case Expression::Id::IfId: DELEGATE(If); + ...... + case Expression::Id::GetGlobalId: DELEGATE(GetGlobal); + case Expression::Id::NopId: DELEGATE(Nop); + case Expression::Id::UnreachableId: DELEGATE(Unreachable); + case Expression::Id::InvalidId: + default: WASM_UNREACHABLE(); + } + + #undef DELEGATE + } +}; +``` +由调用关系我们知道SubType其实最后是受Expression的类型的影响,来自`function->body`
+我们下面看一下Expresssion的类型以及具体的实现,在这里Expression作为Function类内重要
+的成员,其定义如下: +```c++ +class Expression { +public: + enum Id { + InvalidId = 0, + BlockId, + IfId, + LoopId, + ..... + HostId, + NopId, + UnreachableId, + NumExpressionIds + }; + Id _id; + + WasmType type; // the type of the expression: its *output*, not necessarily its input(s) + + Expression(Id id) : _ id(id), type(none) {} + + void finalize() {} + + template + bool is() { + return int(_ id) == int(T::SpecificId); + } +``` +我们自然想到其中枚举的每一个类型都会有一个对应的子类,例如IfId,其对应的子类如下所示:
+```c++ +class If : public SpecificExpression { +public: + If() : ifFalse(nullptr) {} + If(MixedArena& allocator) : If() {} + + Expression* condition; + Expression* ifTrue; + Expression* ifFalse; + + // set the type given you know its type, which is the case when parsing + // s-expression or binary, as explicit types are given. the only additional work + // this does is to set the type to unreachable in the cases that is needed. + void finalize(WasmType type_); + + // set the type purely based on its contents. + void finalize(); +}; +``` +下面我们来看具体的实现 +```c++ +void If::finalize() { + if (condition->type == unreachable) { + type = unreachable; + } else if (ifFalse) { + if (ifTrue->type == ifFalse->type) { + type = ifTrue->type; + } else if (isConcreteWasmType(ifTrue->type) && ifFalse->type == unreachable) { + type = ifTrue->type; + } else if (isConcreteWasmType(ifFalse->type) && ifTrue->type == unreachable) { + type = ifFalse->type; + } else { + type = none; + } + } else { + type = none; // if without else + } +} +``` +上面为对Expression的类型的介绍,对于语言无论什么样的程序块,最后都会有一个类型。现在
+我们以If为例看其如何生成Visitor的,Visitor中的宏展开如下: +```c++ +#define DELEGATE(CLASS_TO_VISIT) \ + return static_cast(this)-> \ + visit##CLASS_TO_VISIT(static_cast(curr)) +-------------------------------------------------------------------- +return static_cast(this)-> \ + visitIf(static_cast(curr)) +``` +这里需要注意RuntimeExpressionRunner::public ExpressionRunner
+虽然在RuntimeExpressionRunner中没有visitIf的实现,但是在public ExpressionRunner中已经
+已经有相关具体的代码实现,这里要区分类的继承关系,具体代码片段如下: +```c++ +Flow visitIf(If *curr) { + NOTE_ENTER("If"); + Flow flow = visit(curr->condition);//先访问具体的条件 + if (flow.breaking()) return flow; + NOTE_EVAL1(flow.value); + if (flow.value.geti32()) { + Flow flow = visit(curr->ifTrue); + //处理是否跳转到else中去继续执行 + if (!flow.breaking() && !curr->ifFalse) flow.value = Literal(); + return flow; + } + if (curr->ifFalse) return visit(curr->ifFalse); + return Flow(); +} +``` +目前我们会比较疑惑,那么例如`a < b`这种是如何处理,这个是在visitBinary上来实现的
+具体代码如下: +```c++ +Flow visitBinary(Binary *curr) { + Flow flow = visit(curr->left); + if (flow.breaking()) return flow; + Literal left = flow.value; + flow = visit(curr->right); + if (flow.breaking()) return flow; + Literal right = flow.value; + .... + case NeInt64: return left.ne(right); + case LtSInt64: return left.ltS(right); + case LtUInt64: return left.ltU(right); + case LeSInt64: return left.leS(right); + case LeUInt64: return left.leU(right); + case GtSInt64: return left.gtS(right); + case GtUInt64: return left.gtU(right); + case GeSInt64: return left.geS(right); + case GeUInt64: return left.geU(right); +} +``` +当然例如加法,减法,位操作等均有具体的case进行处理。
+** 小结 ** +根据上面的描述我们知道了如何从最上层面的函数,到最后的表达计算是如何实现。由于wast是
+表达式形式,这里是一种自顶向向下的计算方式。至此,我们从根本上了解了函数的执行流程。 +#### WAVM底层解释器 +##### ModuleInstance的生成 +WAVM的底层的实现不同于Binaryen,具体的Module的实现在如下的文件中:
+eos\libraries\chain\webassembly\wavm.cpp.我们先看一下是如何生成具体的ModuleInstance的
+具体代码如下: +```c++ +std::unique_ptr module = std::make_unique(); +try { + Serialization::MemoryInputStream stream((const U8*)code_bytes, code_size); + WASM::serialize(stream, *module); +} catch(const Serialization::FatalSerializationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); +} catch(const IR::ValidationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); +} +//上面的代码和Binaryen的没有具体的区别, +eosio::chain::webassembly::common::root_resolver resolver; +//用来解决导入的外部函数符号的问题 +LinkResult link_result = linkModule(*module, resolver); +ModuleInstance *instance = instantiateModule(*module, + std::move(link_result.resolvedImports)); +FC_ASSERT(instance != nullptr); +return std::make_unique(instance, + std::move(module), initial_memory); +``` +这里我们要格外的注意如下代码片段,这段代码内部会调用LLVMJIT的compile方法,把模
+块编译
+成本地可以执行的代码。
+```c++ +ModuleInstance *instance = instantiateModule(*module, + std::move(link_result.resolvedImports)); +``` +函数内部将执行如下代码: +```c++ +LLVMJIT::instantiateModule(module,moduleInstance); +///这段代码展开如下: +-------------------------------------------------- +auto llvmModule = emitModule(module,moduleInstance); +// Construct the JIT compilation pipeline for this module. +auto jitModule = new JITModule(moduleInstance); +moduleInstance->jitModule = jitModule; +// Compile the module. +jitModule->compile(llvmModule); +``` +##### Apply接口实现和调用 +我们具体看下apply函数的代码实现,在实现上他不同用户Binaryen的方式,首先获取函数运行
+的指针,然后初始化相关需要使用的内存,最后调用Invoke来运行函数。 +```c++ +FunctionInstance* call = asFunctionNullable(getInstanceExport(_ instance,entry_point)); +if( !call ) + return; +MemoryInstance* default_mem = getDefaultMemory(_ instance); +if(default_mem) { + resetMemory(default_mem, _ module->memories.defs[0].type); + + char* memstart = &memoryRef(getDefaultMemory(_ instance), 0); + memcpy(memstart, _ initial_memory.data(), _ initial_memory.size()); +} + +the_running_instance_context.memory = default_mem; +the_running_instance_context.apply_ctx = &context; + +resetGlobalInstances(_ instance); +runInstanceStartFunc(_ instance); +Runtime::invokeFunction(call,args); +} +``` +有上main的代码我们得知,其核心的为invokeFunction实现,其实现如何将中间代码进行运行。
+##### InvokeFunction的实现 +我们现在分析一下InvokeFunction函数的实现,并从中我们看一下WAVM是如何实现代码运行的
+```c++ +Result invokeFunction(FunctionInstance* function,const std::vector& parameters) +{ +const FunctionType* functionType = function->type; +//进行简单的参数检查 +if(parameters.size() != functionType->parameters.size()) +{ + throw Exception {Exception::Cause::invokeSignatureMismatch}; +} +//为函数的返回值和参数申请对应的内存 +U64* thunkMemory = (U64*)alloca((functionType->parameters.size() + +getArity(functionType->ret)) * sizeof(U64)); +//检查函数的参数类型 +for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size(); + ++parameterIndex) +{ + if(functionType->parameters[parameterIndex] != parameters[parameterIndex].type) + { + throw Exception {Exception::Cause::invokeSignatureMismatch}; + } + thunkMemory[parameterIndex] = parameters[parameterIndex].i64; +} +//获得函数可以执行的指针,这里将用到LLVM相关的IR技术,后面进行详细介绍 +LLVMJIT::InvokeFunctionPointer invokeFunctionPointer = +LLVMJIT::getInvokeThunk(functionType); +Result result; +Platform::HardwareTrapType trapType; +Platform::CallStack trapCallStack; +Uptr trapOperand; +trapType = Platform::catchHardwareTraps(trapCallStack,trapOperand, + [&] + { + //调用函数,注意这里的invokeFunctionPointer已经为LLVM可以运行 + //的函数指针 + (* invokeFunctionPointer)(function->nativeFunction,thunkMemory); + //获得具体的返回值 + if(functionType->ret != ResultType::none) + { + result.type = functionType->ret; + result.i64 = thunkMemory[functionType->parameters.size()]; + } + }); +} +``` +由上面的代码可以知道其关键的流程是如何从Module描述的代码中得到对应的可以执行的函数
+代码段,我们先在仔细的分析一下其具体实现,可以先参考LLVM官网的实例,这样会更好的理解
+如下函数的具体实现: +```c++ +//cache 重用已经解析过的函数 +auto mapIt = invokeThunkTypeToSymbolMap.find(functionType); +if(mapIt != invokeThunkTypeToSymbolMap.end()) { + return reinterpret_cast(mapIt->second->baseAddress); +} +//-------------------------------------------------------------------------- +//按照LLVM-JIT的要求现,先声称LLVM的Module对象 +auto llvmModule = new llvm::Module("",context); +auto llvmFunctionType = llvm::FunctionType::get( + llvmVoidType, + {asLLVMType(functionType)->getPointerTo(),llvmI64Type->getPointerTo()}, + false); +//-------------------------------------------------------------------------- +//创建function对象,这里会根据FunctionType中的parameter和return的类型来创建对应 +//的函数原型 +auto llvmFunction = llvm::Function::Create( + llvmFunctionType, + llvm::Function::ExternalLinkage, + "invokeThunk", + llvmModule); +auto argIt = llvmFunction->args().begin(); +llvm::Value* functionPointer = &*argIt++; +llvm::Value* argBaseAddress = &*argIt; +//-------------------------------------------------------------------------- +//接着我们创建function的下一个层次,即为block的结构 +auto entryBlock = llvm::BasicBlock::Create(context,"entry",llvmFunction); +llvm::IRBuilder<> irBuilder(entryBlock); +//加载函数的参数, +td::vector structArgLoads; +for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size(); +++parameterIndex) +{ + structArgLoads.push_back(irBuilder.CreateLoad( + irBuilder.CreatePointerCast( + irBuilder.CreateInBoundsGEP(argBaseAddress,{emitLiteral((Uptr)parameterIndex)}), + asLLVMType(functionType->parameters[parameterIndex])->getPointerTo() + ) + )); +} +//调用irBuilder创建本地可以执行的函数指针 +auto returnValue = irBuilder.CreateCall(functionPointer,structArgLoads); + +// 如果有返回值,则创建存储,并存储返回值. +if(functionType->ret != ResultType::none) +{ + auto llvmResultType = asLLVMType(functionType->ret); + irBuilder.CreateStore( + returnValue, + irBuilder.CreatePointerCast( + irBuilder.CreateInBoundsGEP(argBaseAddress, + {emitLiteral((Uptr)functionType->parameters.size())}), + llvmResultType->getPointerTo() + ) + ); +} + +irBuilder.CreateRetVoid(); + +//连接调用的函数 +auto jitUnit = new JITInvokeThunkUnit(functionType); +jitUnit->compile(llvmModule); +//最后返回结果,只要将native可以执行的代码传递给这个指针函数就可以运行 +return reinterpret_cast(jitUnit->symbol->baseAddress); +``` +由上面的代码我们得知,LLVMJIT的方式主要还是依赖于LLVM将函数代码编译成本地代码,而上层
+需要我们创建Module->Function->Block的这种关系,和对应的参数。 +## 总结 +本文深入的分析了EOS虚拟的实现所涉及到的相关技术,从上层到底层,从客户端到服务端分析了
+EOS中智能合约是如何运行和落地的,希望对你有所帮助。 + + +--- diff --git a/VM/EOS/image/CommonComplier.png b/VM/EOS/image/CommonComplier.png new file mode 100644 index 0000000..f12e441 Binary files /dev/null and b/VM/EOS/image/CommonComplier.png differ diff --git a/VM/EOS/image/EOSApplyCTX.png b/VM/EOS/image/EOSApplyCTX.png new file mode 100644 index 0000000..2beb62b Binary files /dev/null and b/VM/EOS/image/EOSApplyCTX.png differ diff --git a/VM/EOS/image/ExampleOfDTF.png b/VM/EOS/image/ExampleOfDTF.png new file mode 100644 index 0000000..478e588 Binary files /dev/null and b/VM/EOS/image/ExampleOfDTF.png differ diff --git a/VM/EOS/image/ExampleOfTF.png b/VM/EOS/image/ExampleOfTF.png new file mode 100644 index 0000000..ccd8b43 Binary files /dev/null and b/VM/EOS/image/ExampleOfTF.png differ diff --git a/VM/EOS/image/LLVMArchFull.jpg b/VM/EOS/image/LLVMArchFull.jpg new file mode 100644 index 0000000..df0f140 Binary files /dev/null and b/VM/EOS/image/LLVMArchFull.jpg differ diff --git a/VM/EOS/image/LLVMArchSimple.png b/VM/EOS/image/LLVMArchSimple.png new file mode 100644 index 0000000..3e055ac Binary files /dev/null and b/VM/EOS/image/LLVMArchSimple.png differ diff --git a/VM/EOS/image/LLVMFormatFileTrans.jpg b/VM/EOS/image/LLVMFormatFileTrans.jpg new file mode 100644 index 0000000..0c26fc3 Binary files /dev/null and b/VM/EOS/image/LLVMFormatFileTrans.jpg differ diff --git a/VM/EOS/image/RuntimeInterface.jpg b/VM/EOS/image/RuntimeInterface.jpg new file mode 100644 index 0000000..142c318 Binary files /dev/null and b/VM/EOS/image/RuntimeInterface.jpg differ diff --git a/VM/EOS/image/WASMInWebBrowser.png b/VM/EOS/image/WASMInWebBrowser.png new file mode 100644 index 0000000..9cb7750 Binary files /dev/null and b/VM/EOS/image/WASMInWebBrowser.png differ diff --git a/VM/EOS/image/WASMToHTMLFlow.jpg b/VM/EOS/image/WASMToHTMLFlow.jpg new file mode 100644 index 0000000..2b2a950 Binary files /dev/null and b/VM/EOS/image/WASMToHTMLFlow.jpg differ diff --git a/VM/EVM/EVMArch.md b/VM/EVM/EVMArch.md new file mode 100644 index 0000000..3530b9d --- /dev/null +++ b/VM/EVM/EVMArch.md @@ -0,0 +1,536 @@ +### EVM架构简析和源码分析 +EVM为以太坊虚拟机。以太坊底层通过EVM模块支持智能合约的执行和调用,调用时根据合约的地址获
+取到代码,生成具体的执行环境,然后将代码载入到EVM虚拟机中运行。通常目前开发智能合约的高级
+语言为Solidity,在利用solidity实现智能合约逻辑后,通过编译器编译成元数据(字节码)最后发布到以
+坊上。 + +### EVM架构概述 +EVM本质上是一个堆栈机器,它最直接的的功能是执行智能合约,根据官方给出的设计原理,EVM
的主要的设计目标为如下几点: +* 简单性 +* 确定性 +* 空间节省 +* 为区块链服务 +* 安全性保证 +* 便于优化 + +针对以上几点通过对EVM源代码的阅读来了解其具体的设计思想和工程实用性。 + +### EVM存储系统 +#### 机器位宽 +EVM机器位宽为**256**位,即32个字节,256位机器字宽不同于我们经常见到主流的64位的机器字宽,
这就标明EVM设计上将考虑一套自己的关于操作,数据,逻辑控制的指令编码。目前主流的处理器
原生的支持的计算数据类型有:8bits整数,16bits整数,32bits整数,64bits整数。一般情况下宽
字节的的计算将更加的快一些,因为它可能包含更多的指令被一次性加载到pc寄存器中,同时伴有
内存访问次数的减少。目前在X86的架构中8bits的计算并不是完全的支持(除法和乘法),但基本
的数学运算大概在几个时钟周期内就能完成,也就是说主流的字节宽度基本上处理器能够原生的支
持,那为什么EVM要采用256位的字宽。主要从以下两个方面考虑: +* 时间,智能合约是否能执行得更快 +* 空间,这样是否整体字节码的大小会有所减少 +* gas成本 + +**时间**上主要体现在执行的效率上,我们以两个整形数相加来对比具体的操作时间消耗。
+*32bits相加的X86的汇编代码* +``` +mov eax, dword [9876ABCD] //将地址9876ABCD中的32位数据放入eax数据寄存器 +add eax, dword [1234DCBA] //将1234DCBA地址指向32位数和eax相加,结果保存在eax中 +``` +*64bits相加的X86汇编代码* +``` +mov rax, qword [123456789ABCDEF1] //将地址指向的64位数据放入64位寄存器 +add rax, qword [1020304050607080] //计算相加的结果并将结果放入到64位寄存器中 +``` +下面我们看一下在64bits机器上如何完成256bits的加法 +``` +mov rax, qword [9876ABCD] +add qword [1234DCBA], rax +mov rax, qword [9876ABCD+8] +adc qword [1234DCBA+8], rax//这里应用adc带进位的加法指令,影响进位标记CF +mov rax, qword [9876ABCD+16] +adc qword [1234DCBA+16], rax +mov rax, qword [9876ABCD+24] +adc qword [1234DCBA+24], rax +``` +由上面的的汇编指令我们可以看出256位操作要比系统原生支持的要复杂的多,从时间上考虑采用
+256位这样的字节宽度,实际的收益并不大。

+**空间**上,由上面的汇编操作(在实际的EVM中操作类似)我们不难看到,如果直接对地址进行操
+作似乎是一种快速的方式,并减少了操作数,进而操作码也有所减少,相应的智能合约的字节流
+大小就会小很多,gas花费也会有所下降。但是从另外一个层面来讲,支持宽字节的数据类型势必
+会造成在处理低字节宽度的数据时候带来存储上的浪费(如添加标识用来区分类型)或者添加额外
+的操作来进行数据的compact。

+从时间和空间角度来看,仅支持256字节宽度的选择有利有弊,具体还要看以太坊智能合约的具体
应用。可能的几点原因如下:
+* 256位的宽度方便进行密码学方面的计算(sha256),但是成本有些高,场景比较少 +* 仅支持256位的比要支持其他类型的操作要少,单一,实现简单可控 +* 和gas的计算相关,仅支持一种,方便计算,同时也考虑到了安全问题 + +#### 内存分配 +EVM中数据可以在三个地方进行存储,分别是栈,临时存储,永久存储。
+由于EVM是基于栈的虚拟机,因此基本上所有的操作都是在栈上进行的,并且EVM中没有寄存器的概
念, +这样EVM对栈的依赖就更大,虽然这样的设计使实现比较简单且易于理解,但是带来的问题就是需
+要更多数据的相关操作。在EVM中栈是唯一的免费(几乎是)存放数据的地方。栈自然有深度的限制,
目前的限制是1024
+`static constexpr int64_t stackLimit = 1024;`
+因为栈的限制,因此栈上的临时变量的使用会受限制。 +临时内存存储在每个VM实例中,并在合约执行
完后消失 +永久内存存储在区块链的状态层。 + +--- +### EVM代码简析 +本文分析EVM c++的代码, github地址为https://github.com/ethereum/cpp-ethereum
+EVM c++实现了同go版本中的核心的功能,对于快速理解EVM的设计思想比较受用,对于熟悉C++的
+可以先通过阅读C++版本的实现,然后对标go版本的实现来进一步了解EVM的设计思路。
+#### EVM代码overview +主要代码路径为cpp-ethereum/libevm和libethereum,代码量不大,以下为主要代码。
+``` +EVMC.cpp +ExtVMFace.cpp +Instruction.cpp +interpreter.h +LegacyVMCalls.cpp +VM.cpp +VMCalls.cpp +VMFactory.cpp +VMOpt.cpp +VMSIMD.cpp +VmValidate.cpp +LegacyVM.cpp +Executive.cpp +ExtVm.cpp +``` +下图为EVM中主要类的类图:
+ +![MainClassDiagram](./image/MainClass.jpg "MainClass") + +以太坊中调用EVM相关代码的入口存在于不同的阶段,我们主要从以下几个角度来看它是如何运作的
+* 执行环境创建 +* 新合约的创建 +* 合约执行调用、 +* 执行后结果返回 +* 运行过程中的gas消费 + +**执行环境的创建**
+以太坊虚拟中涉及到的虚拟机执行的关键类为Executive(位于目录libethereum)中,我们先从
+创建运行环境入手,关键入口函数call入手,函数声明为:
+``` +bool call(Address const& _receiveAddress, //接收者地址 + Address const& _txSender, //发送者地址 + u256 const& _txValue, //transaction的值 + u256 const& _gasPrice, //gas的价格 + bytesConstRef _txData, //具体的transaction数据 + u256 const& _gas); //提供的gas值 +bool call(CallParameters const& _cp, //运行参数,由上个函数的相关变量创建 + u256 const& _gasPrice, //gas的价格 + Address const& _origin); //源地址 +``` +两个函数的功能是一样的,只是第一个函数会将参数包装成CallParameters然后在调用第二个函数,现在看
一下call函数的流程图如下:
+ +![ExecutiveCallFlowDiagram](./image/ExecutiveCallFlowChart.jpg "ExecutiveCallFlowDiagram")
+上述函数的关键点在于在不是预编译合约(以太坊中内部定义好的合约多为签名计算),生成**ExtVM**
实例,这个对象将在函数go中返回重要的作用。注意函数最后会更改状态指向函数transferBalance:
+` m_s.transferBalance(_p.senderAddress, _p.receiveAddress, _p.valueTransfer);`
+ +**新合约的创建**
+如果调用的合约地址在系统中还没有出存在,这个适合涉及到合约的创建,关键函数为create:
+``` +bool create(Address const& _txSender, //目的地址 + u256 const& _endowment, //花费的值 + u256 const& _gasPrice, //gas的价钱 + u256 const& _gas, //花费的gas数量 + bytesConstRef _init, //传入的字节码 + Address const& _origin) //源地址 +``` +函数将调用createOpcode函数,该函数在获得sender的nonce值后生成合约地址,并且调用函数
executeCreate来完成最后的创建。 +``` +u256 nonce = m_s.getNonce(_sender); +//注意地址保存在m_newAddress中,这个是否足够灵活 +m_newAddress = right160(sha3(rlpList(_sender, nonce))); +return executeCreate(_sender, _endowment, _gasPrice, _gas, _init, _origin); +``` +下面看关键的的函数executeCreate,其流程图如下所示:
+![ExecutiveCeateFlowChart](./image/ExecutiveCreateFlowChart.jpg "ExecutiveCreateFlowChart")
+我们从代码中不难看出,最后和call函数相同生成ExtVM对象,将要执行的字节码放入其中。
这里需要注意的是执行transferBalance时候如果没有账户将创建一个新的账户。
+**合约执行调用**
+合约的执行的起始点在函数Executive::go中,函数的流程图如下:
+![ExecutiveGoFlowChart](./image/ExecutiveGoFlowchart.jpg "ExecutiveGoFlowChart")
+从流程图中我们看到如果m_ext(create或者call函数中创建)不为空的时候通过工厂方法创建VM,
这里要正确的理解m_ext和vm的区别,我们先看工厂方法的实现:
+``` +std::unique_ptr VMFactory::create(VMKind _kind) +{ + switch (_kind) + { +#ifdef ETH_EVMJIT + case VMKind::JIT: + return std::unique_ptr(new EVMC{evmjit_create()}); +#endif +#ifdef ETH_HERA + case VMKind::Hera: + return std::unique_ptr(new EVMC{evmc_create_hera()}); +#endif + case VMKind::Interpreter: + return std::unique_ptr(new EVMC{evmc_create_interpreter()}); + case VMKind::DLL: + return std::unique_ptr(new EVMC{g_dllEvmcCreate()}); + case VMKind::Legacy: + default: + return std::unique_ptr(new LegacyVM); + } +} +``` +从代码中我们不难看出,会根据虚拟机的类型创建对应的虚拟机对象,无参函数create将默认生成 +
类型为VMKind::Legacy。在函数create和call中生成的m_ext是虚拟机和外部相关状态进行
交互的。调用VM的exec函数完成虚拟机的执行。LegacyVM的exec函数关键代码如下:
+``` +// trampoline to minimize depth of call stack when calling out +m_bounce = &LegacyVM::initEntry;//初始化m_bounce +do + (this->*m_bounce)(); +while (m_bounce); +``` +初始化m_bounce的函数使initEntry,我们看下initEntry函数中实现的功能。
+``` +m_bounce = &LegacyVM::interpretCases;//设置m_bounce为函数interpreCases +initMetrics();//初始化操作,参数和返回值,gas花费矩阵, +optimize();//优化,主要针对跳转的优化,后续再做补充,目前程序汇总关闭 +``` +经过上面的代码处理,m_bounce设置为下一步将要运行的函数。初始化操作以及花费说明的矩阵,
矩阵关联的Instruction部分相关信息如下:
+``` +static const std::map c_instructionInfo = +{ // Add, Args, Ret, GasPriceTier + { Instruction::STOP, { "STOP", 0, 0, 0, Tier::Zero } }, + { Instruction::ADD, { "ADD", 0, 2, 1, Tier::VeryLow } }, + { Instruction::SUB, { "SUB", 0, 2, 1, Tier::VeryLow } }, + { Instruction::MUL, { "MUL", 0, 2, 1, Tier::Low } }, + { Instruction::DIV, { "DIV", 0, 2, 1, Tier::Low } }, + { Instruction::SDIV, { "SDIV", 0, 2, 1, Tier::Low } }, + { Instruction::MOD, { "MOD", 0, 2, 1, Tier::Low } }, +``` + +经过上面的相关初始化,再运行循环将执行m_bounce指向的interpretCases函数。函数interpretCase
相对来说比较复杂一些,如下所示:
+``` +void LegacyVM::interpretCases() +{ + INIT_CASES + DO_CASES + { + CASE(CREATE2) + { + ON_OP(); + if (!m_schedule->haveCreate2) + throwBadInstruction(); + m_bounce = &LegacyVM::caseCreate; + } + BREAK + CASE(CREATE) + ........ + 此处省略很多的case + ....... + + NEXT + + CASE(INVALID) + DEFAULT + { + throwBadInstruction(); + } + } + WHILE_CASES +} +``` +上述代码中对应了很多宏,,如果不对宏进行展开,直观理解就是对每种操作执行对应的函数,并将
结果进行中间存储,EVM中定义了相关宏开关,其中一些宏开关,都对应了上述代码中不同的
宏展开,同时对应了不同的代码组织形式。参见文件VMConfing.h,如下代码为相关的宏开关:
+``` +EIP_615 - 子程序和静态跳转的方式 +EIP_616 - 单执行多数据流的方式 +EVM_OPTIMIZE - 优化开关,当值为false时,所有的优化全部关掉 +EVM_SWITCH_DISPATCH - 通过loop和switch执行代码 +EVM_JUMP_DISPATCH - 跳转通过一个跳表来实现,只针对gcc +EVM_USE_CONSTANT_POOL - 应用静态数据并在栈上直接赋值操作 +EVM_REPLACE_CONST_JUMP - 带预确认的跳转来保证运行时的循环 +EVM_TRACE - 提供不同等级的trace操作 +``` +下面我们来看一下源代码中是如何设置这些宏开关
+``` +首先EIP_615和EIP_616均为关闭状态 +#ifndef EIP_615 +#define EIP_615 false +#endif + +#ifndef EIP_616 +#define EIP_616 false +#endif +//-------------------------------------------------------------------------- +//如果没有定义EVM_JUMP_DISPATCH的情况下,如果是GNU的gcc,则打开EVM_JUMP_DISPATCH +//我们一般在linux编译,则这里我们可以得到的结论是开关EVM_JUMP_DISPATCH开关打开 +ifndef EVM_JUMP_DISPATCH +#ifdef __GNUC__ +#define EVM_JUMP_DISPATCH true +#else +#define EVM_JUMP_DISPATCH false +#endif +#endif + +#if EVM_JUMP_DISPATCH +#ifndef __GNUC__ +#error "address of label extension available only on Gnu" +#endif +#else +#define EVM_SWITCH_DISPATCH true +#endif +//--------------------------------------------------------------------------- +//从下面的宏定义EVM_OPTIMIZE的开关是关闭的,因此开关EVM_REPLACE_CONST_JUMP
+//EVM_USE_CONSTANT_POOL EVM_DO_FIRST_PASS_OPTIMIZATION均为false的状态 +#ifndef EVM_OPTIMIZE +#define EVM_OPTIMIZE false +#endif +#if EVM_OPTIMIZE +#define EVM_REPLACE_CONST_JUMP true +#define EVM_USE_CONSTANT_POOL true +#define EVM_DO_FIRST_PASS_OPTIMIZATION \ + (EVM_REPLACE_CONST_JUMP || EVM_USE_CONSTANT_POOL) +#endif +``` +综上宏开关的说明,最后开关EVM_JUMP_DISPATCH是打开的,其他的均处于关闭状态。 + +在开关**EVM_JUMP_DISPATCH**打开的情况下对每一个宏展开进行说明,便于理解实际是如何运
行的。具体的参见文件VMConfig.h
+***宏INIT_CASES***
+在开关EVM_JUMP_DISPATCH打开的情况下该宏展开为初始化一个静态的跳转表,代码片段如下:
+``` +#define INIT_CASES \ + \ + static const void* const jumpTable[256] = { \ + &&STOP, /* 00 */ \ + &&ADD, \ + &&MUL, \ + &&SUB, \ + &&DIV, \ + &&SDIV, \ + &&MOD, \ + &&SMOD, \ + &&ADDMOD, \ +``` +其中的ADD等定义在文件Instruction.h中,其中枚举了所有的虚拟机执行过程中的字节操作码。
+``` +/// Virtual machine bytecode instruction. +enum class Instruction: uint8_t +{ + STOP = 0x00, ///< halts execution + ADD, ///< addition operation + MUL, ///< mulitplication operation + SUB, ///< subtraction operation + DIV, ///< integer division operation + SDIV, ///< signed integer division operation + ....... + ....... +``` + +***宏DO_CASES***
+在开关EVM_JUMP_DISPATCH打开的情况下该宏展开为如下代码片段:
+``` +#define DO_CASES \ + fetchInstruction(); \ + goto* jumpTable[(int)m_OP]; +``` + +由上面的代码可知操作符定义为一个字节,函数fetchInstruction执行后m_OP中存储了当前要
+执行的操作,接着switch将根据具体的操作来执行分支中的内容。
+***宏CASES***
+在开关EVM_JUMP_DISPATCH打开的情况下CASES展开的内容如下 :
+``` +#define CASE(name) \ + name: +``` +***宏NEXT***
+在开关EVM_JUMP_DISPATCH打开的情况下NEXT展开的内容如下 :
+``` +#define NEXT \ + ++m_PC; \ + fetchInstruction(); \ + goto* jumpTable[(int)m_OP]; +``` +***宏CONTINUE***
+在开关EVM_JUMP_DISPATCH打开的情况下CONTINUE展开的内容如下 :
+``` +#define CONTINUE \ + fetchInstruction(); \ + goto* jumpTable[(int)m_OP]; +``` +其他的几个控制分支跳转的宏展开如下:
+``` +#define BREAK return; +#define DEFAULT +#define WHILE_CASES +``` +关键的执行操作的宏ON_OP展开如下:
+``` +#define ON_OP() onOperation() +``` +通过上面的宏的展开我们可以看出过程主要依赖jumpTable和函数fetchInstructio来完成
+执行过程。首先我们看一下函数fetchInstruction的具体执行流程,整个执行将从这里开始
+``` +//初始化时m_PC为0,这里先获取第一个操作符 +m_OP = Instruction(m_code[m_PC]); +//获取操作相关的参数,gas花费等信息 +const InstructionMetric& metric = c_metrics[static_cast(m_OP)]; +//设置SP为最后返回值所在的位置,同时检查参数出栈和返回值入栈没有超过栈的边界 +adjustStack(metric.args, metric.ret); + +//计算运行的费用 +m_runGas = toInt63( + m_schedule->tierStepGas[static_cast(metric.gasPriceTier)] + ); +m_newMemSize = m_mem.size(); +m_copyMemSize = 0; +``` +下面我们看程序是如何利用跳表和onOpearion来完成程序的执行,下面我们以ADD操作
为例来进行说明,在进行ADD说明前,先简单介绍一下EVM中栈工作的原理,例如如果我
们要执行一 +加法操作,则通常表示为c = a + b,把它翻译成EVM中栈的操作序列伪操作码
序列如下:
+``` +push a into stack +push b into stack +pop a and b then cal a+b +push a+b into stack +``` +我们先看一下ADD的前置操作PUSH是如何实现的,首先注意如下代码:
+````` + CASE(PUSH2) + ......... + CASE(PUSH23) + CASE(PUSH24) + CASE(PUSH25) + CASE(PUSH26) + CASE(PUSH27) + CASE(PUSH28) + CASE(PUSH29) + CASE(PUSH30) + CASE(PUSH31) + CASE(PUSH32) + { + ON_OP(); + updateIOGas(); + int numBytes = (int)m_OP - (int)Instruction::PUSH1 + 1; + m_SPP[0] = 0; + for (++m_PC; numBytes--; ++m_PC) + //这里主要是处理256位宽的的情况 + m_SPP[0] = (m_SPP[0] << 8) | m_code[m_PC]; + } + CONTINUE +````` +EVM中对PUSH1的其他操作均采用上面的代码,PUSH3的含义是将三个输入的字节序列中的宽度
为32字节的数据压入到栈中,如下代码:
+`m_SPP[0] = (m_SPP[0] << 8) | m_code[m_PC];`
+上面这段代码主要是将传入的字节流数据转化为256的数据,并放入栈上,同时我们的栈的定义如下:
+`u256 m_stack[1024];` +这里的设计其实有可以改进的地方,主要是以下几点:
+* 即使编译器部分做了数据紧凑的优化,但是执行的过程中又做了放大,失去了原来的意义。 +* 栈的宽度为256位,抛开加密算法的影响(目前几乎没有栈上运算),实际有些浪费。 +* 从实际角度讲可以选择小的位宽的栈,数据更加紧密,但是可能会造成操作增加。 + +通过执行完PUSH操作后,栈上的已经放好了我们要做加法的数据,现在我们看一下ADD操作是如何
的。如下为ADD操作的代码片段:
+``` +CASE(ADD) +{ + ON_OP(); + updateIOGas(); + + //pops two items and pushes their sum mod 2^256. + m_SPP[0] = m_SP[0] + m_SP[1]; +} +NEXT +``` +上面的代码通过宏展开后代码如下:
+``` +ADD: +{ + onOperation(); + updateIOGas(); + m_SPP[0] = m_SP[0] + m_SP[1]; +} +++m_PC; +fetchInstruction(); +goto* jumpTable[(int)m_OP]; +``` +现在step by step的看一下这部分是如何进行处理,首先是opOperaion函数,如下为onOperation
函数的实现:
+``` +if (m_onOp) + (m_onOp)(++m_nSteps, m_PC, m_OP, + m_newMemSize > m_mem.size() ? (m_newMemSize - m_mem.size()) / 32 : uint64_t(0), + m_runGas, m_io_gas, this, m_ext); +``` +代码中的m_onOp为提供的回调函数,其中其中m_onOp的类型为OnOpFunc,声明如下如下所示:
+``` +using OnOpFunc = std::function; +``` +函数设计主要是在虚拟执行的时候提供一个可以回调的接口,方便进行处理,比如做最简单的tracing
+接下来执行函数updateIOGas(),该函数功能主要是查看目前的gas消耗是否已经大于提供的gas,
如果大于,则抛出异常,虚拟机停止运行。代码片段如下:
+``` +if (m_io_gas < m_runGas) + throwOutOfGas(); +m_io_gas -= m_runGas; +``` +注意函数中的m_runGas在函数updateGas中被修改,会根据内存的使用来进行计算和消耗。 +接下来执
行的代码为`m_SPP[0] = m_SP[0] + m_SP[1];`主要功能是完成两个数的相加计算结果会存储在m_SPP[0]
中,m_SPP的含义是“指向下一个栈中可用的位置”,这里指向m_SP[0]运行初始化开始时m_SPP = m_SP。
+接着执行`++m_PC`,这里将指向代码字节流中的下一个操作。接着执行数fetchInstruction(), +这个时候
变量m_OP中存储的是新的操作,继续执行goto* jumpTable[(int)m_OP],这个时候将跳到下一个操作的
label去执行。 +综上,EVM虚拟机就是借助栈和基本的操作来完成一个合约字节流的运行的。
+**执行后的结果返回**
+在操作序列的RETURN和REVERT操作,将栈上的结果数据拷贝到内存中去。以下为RETURN部分代码/br> +``` +CASE(RETURN) +{ + ON_OP(); + m_copyMemSize = 0; + updateMem(memNeed(m_SP[0], m_SP[1])); + updateIOGas(); + + uint64_t b = (uint64_t)m_SP[0]; + uint64_t s = (uint64_t)m_SP[1]; + //m_output中存储的就是最后返回的结果 + m_output = owning_bytes_ref{std::move(m_mem), b, s}; + m_bounce = 0; +} +BREAK +``` +**运行过程中的Gas的消费**
+上面描述ADD和所涉及到的PUSH操作的时候我们就已经看到会调用updateIOGas函数来计算gas
的消费。从代码中我们可以看到不同的操作对应不同的gas消费具体如下:
+ + + no gas | IO gas 1| IO gas 2 | mem gas + :--|:--|:--|:-- + CREATE2|RETURN|REVERT|RETURN + CREATE|SUICIDE|STOP|REVERT +DELEGATECALL|MLOAD|MSTORE|MLOAD +STATICCALL|MSTORE8|SHA3|MSTORE +CALL|LOG0|LOG1|MSTORE8 +CALLCODE|LOG2|LOG3|SHA3 +JUMPTO|EXP|ADD|LOG1 +JUMPIF|MUL|SUB|LOG2 +JUMPV|DIV|SDIV|LOG3 +JUMPSUB|MOD|SMOD|CALLDATACOPY +JUMPSUBV|NOT|LT| +RETURNSUB|GT|SLT| +BEGINSUB|SGT|EQ| +BEGINDATA|ISZERO|AND| +GETLOCAL|OR|XOR| +PUTLOCAL|BYTE|SHL| +|SHR|SAR| +|ADDMOD|MULMOD| +|SIGNEXTEND|ADDRESS| +|ORIGIN|BALANCE| +|CALLER|CALLVALUE| +|CALLDATALOAD|CALLDATASIZE| +|RETURNDATASIZE|CODESIZE| +|EXTCODESIZE|CALLDATACOPY| +|RETURNDATACOPY|RETURNDATACOPY| +|CODECOPY|CODECOPY| +|GASPRICE|BLOCKHASH| +|COINBASE|TIMESTAMP| +|NUMBER|DIFFICULTY| +|GASLIMIT|POP| +|PUSH|JUMP| +|DUP|SWAP| +|SLOAD|SSTORE| +|PC|MSIZE| +|GAS|JUMPDEST| +|INVALID|| +其中**SSTORE**是一条比较特殊的指令,他将消耗stack存储的gas
+ +###小结 +EVM如果精确的定位应该是一个基于栈的自定义字节码解释器,实现上并不复杂,代码
过程中遇到的主要问题有以下几点:
+* 字节宽度的设计,目前看主要是出于实现难度和实际需求(如SHA3的操作)。 +* 设计上每次都会生成新的VM对象,出于安全和目前需求这样的设计和实现是可以接受的,数据
共享和多线程处理在智能合约的层面目前还不需要。 +* EVM整体上实现了基本的操作,但是还需要和语言编译器去结合来做具体的优化。 +* 目前下一代的EVM虚拟机eWASM在设计中,目前GitHub上只有设计并没有代码实现。 diff --git a/VM/EVM/EVMLibIntroduction.md b/VM/EVM/EVMLibIntroduction.md new file mode 100644 index 0000000..e9c46ca --- /dev/null +++ b/VM/EVM/EVMLibIntroduction.md @@ -0,0 +1,94 @@ +# 让你的区块链兼容以太坊智能合约 +## 引言 +随着区块链技术以及应用的普及,越来越多的区块链出现在大众视野中。由于区块链技术的开源特性,任何公司和个人都可以方便快捷的获取最新的区块链核心技术,通过对这些技术的选择和整合,最后开发和搭建满足特定业务需求的区块链产品。以太坊作为目前区块链2.0的杰出代表被作为诸多区块链项目开发的基础,甚至有人统计100个区块链项目中有94个是基于以太坊,而以太坊社区更是有25万的开发者在活跃着,因此以太坊成为大家争相研究和进行区块链开发的典型。EVM作为以太坊中重要的组件,其运行着以太坊上至关重要的智能合约,由于以太坊庞大的社区和经济环境,作为一个新出现的区块链兼容以太坊的智能合约逐渐的变为一种显性的需求,本文将介绍一个区块链兼容以太坊智能合约的思路和具体的实现。 +## 实现思路 +以太坊虚拟机作为一个图灵完备的虚拟机有其独特的优势和特点,这些在公众号之前的文章有所介绍。运行在以太坊上的智能合约实现了以太坊上丰富的应用,无论是发币,博彩还是游戏这些都离不开智能合约的运行,离不开虚拟机这个运行载体。如果你要开发一条新的区块链,无论你是否基于以太坊进行开发,虚拟机与智能合约的支持是必须要考虑的问题。为了吸引以太坊用户或者Dapp的开发和发布人员,同时也是出于基于以太坊对业务迁移成本的考虑,兼容以太坊智能合约往往都会写入到白皮书中。 +对于兼容以太坊智能合约有以下两种实现方式: +* 编译器层面支持,支持将以太坊的智能合约编译成自实现的虚拟机可以操作执行的字节码 +* 虚拟机层面支持,虚拟机支持解析以太坊智能合约编译成的字节码 + +通过简单的考虑,我们不难发现在虚拟机层面支持将是成本最低的方案。现在我们回到以太坊虚拟机,之前的文章有详细的介绍过以太坊虚拟机的实现和运行模式,对于每个智能合约的运行将创建一个新的EVM实例,这给我提供了一个思路:通过对以太坊虚拟机部分功能的剥离,这样我们就可以得到一个以太坊智能合约的运行环境,代码层面就是一个EVM的函数库。这样在新的区块链中,如果我们希望区块链兼容以太坊智能合约,我们只需要实现该函数库对外的接口,并将智能合约二进制码以参数的形式传递进去(这也是大多数虚拟机的方式),并以二进制的形式获得输出,这样我们就实现了对以太坊智能合约的兼容。 +基于以上的思路,我们主要做了以下的工作: +* 剥离go-ethereum中的EVM部分代码为单独的工程 +* 尽量的去除EVM中的对go-ethereum的编译依赖 +* 梳理EVM中运行的对外的依赖接口 +* 提供一个完整的可以二次开发的EVM,即以太坊智能合约运行环境 + +以上工作过程中的主要原则是尽量的使代码有少的外部依赖,这样做的主要目的一是工程上方便实现该函数库的二次开发,二是减少使用者的二次开发成本。 +## 具体实现 +可以在github上获取该项目的源代码,你将得到一个最小的以太坊智能合约运行环境,github地址为:https://github.com/XChainLab/evm-lite.git +工程下主要有三个目录: +* crypto为加密函数库,函数库来源于go-ethereum,这部分单独出目录 +* kernal为以太坊虚拟机核心代码,实现了智能合约的运行环境,代码来自go-ethereum +* demo为一个具体的使用示例 + +通过demo我们来演示如何让你的区块链支持以太坊智能合约
+### 第一步实现数据访问接口 +由于不同区块链底层依赖的数据存储不同,而以太坊智能合约中有对存储的操作,因此这里我们需要实现数据访问接口,接口的描述见文件kernal/statedb.go。 +demo中我们实现了其中的部分接口,具体见mockstatedb.go,这里需要说明一下,demo中实现的是以太坊智能合约运行必须实现的接口,其他接口可以考虑不实现,必要的接口函数为如下: +``` +GetCode(address kernal.Address) []byte +GetCodeHash(kernal.Address) kernal.Hash +SetCode(address kernal.Address, data []byte) +GetCodeSize(address kernal.Address) int +Exist(kernal.Address) bool +Empty(kernal.Address) bool +//关于snapshot的接口需要根据具体情况进行实现 +RevertToSnapshot(int) +Snapshot() int +HaveSufficientBalance(kernal.Address, *big.Int) bool +TransferBalance(kernal.Address, kernal.Address, *big.Int) +``` +除此之外还要实现一个链访问的接口,具体见kernal/chain.go,这里只需要实现一个接口函数即可 +``` +GetBlockHeaderHash(uint64) kernal.Hash +``` +### 第二步创建EVM执行实例 +具体见demo/runtime.go,这里主要工作是初始化相关的配置,该项目的原则上保留了以太坊的相关配置,使用者可以根据自己的情况设置其中的具体数值,demo中采用的均是默认值,使用者可以进行参考,创建EVM部分的代码如下: +``` +func CreateExecuteRuntime(caller kernal.Address) *kernal.EVM { + context := CreateExecuteContext(caller) + stateDB := MakeNewMockStateDB() + chainConfig := CreateChainConfig() + vmConfig := CreateVMDefaultConfig() + chainHandler := new(ETHChainHandler) + + evm := kernal.NewEVM(context, stateDB, chainHandler, chainConfig, vmConfig) + return evm +} +``` +### 第三部调用智能合约 +在第二步中我们创建了EVM的运行实例,这里我们通过调用EVM的Call函数直接运行代码的方式来运行智能合约 +``` +HexTestCode := "6060604052600a8060106000396000f360606040526008565b00" +TestInput := []byte("Contract") +TestCallerAddress := []byte("TestAddress") +TestContractAddress := []byte("TestContract") +calleraddress := kernal.BytesToAddress(TestCallerAddress) +contractaddress := kernal.BytesToAddress(TestContractAddress) +evm := CreateExecuteRuntime(calleraddress) +evm.StateDBHandler.CreateAccount(contractaddress) +evm.StateDBHandler.SetCode(contractaddress, kernal.Hex2Bytes(HexTestCode)) +caller := kernal.AccountRef(evm.Origin) +ret, _, err := evm.Call( + caller, + contractaddress, + TestInput, + evm.GasLimit, + new(big.Int)) +if err != nil { + fmt.Println(err) +} else { + fmt.Println(ret) +} +``` +这里我们直接将代码传递给了EVM,目前EVM对外的接口保留源代码中的各个接口,可以通过调用Create函数来实现创建一个智能合约。 +### 编译运行 +执行上面的demo十分的简单主要执行以下的几步操作即可: +* 确认你的机器上有golang的编译环境 +* git clone 代码到你的机器的任何路径,无需放到GOPATH下 +* 进入demo文件夹,执行go build命令 +* 运行demo即可 + +## 总结 +本文通过以上的说明提供了一个让你的区块链支持以太坊虚拟机的思路和实现方式,并提供一个EVM的纯净版本供开发者使用,使开发者可以快速的在一天的时间里完成区块链对以太坊智能合约支持的开发,后续我们将结合目前区块链虚拟机技术的发展方向,来不断的提供对虚拟机通用化的技术支持和社区贡献。 diff --git a/VM/EVM/evm.md b/VM/EVM/evm.md new file mode 100644 index 0000000..af00f7a --- /dev/null +++ b/VM/EVM/evm.md @@ -0,0 +1,95 @@ + +#evm概述 +-- + + + + +#### evm + +* [1.evm语言虚拟机](#1) +* [2.虚拟机设计组成](#2) +* [3.evm设计目标](#3) +* [4.evm优缺点](#4) +* [5.IELE设计原理](#5) +* [6.evm和其他语言虚拟机区别](#6) + + +

1.evm语言虚拟机

+ +* evm本质上就是一个简单的语言虚拟机,常见的语言虚拟机有jvm,lvm。语言虚拟机主要实现了隔离技术,就是指在底层实现了环境隔离,它屏蔽了与具体操作系统平台相关的信息,使得程序只在虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 + +* EVM使用了256比特长度的机器码,是一种基于堆栈的虚拟机,用于执行智能合约,并使用了以太坊账户模型(Account Model)来进行价值传输。 +* EVM是图灵完备的,由于以太坊系统中引入了Gas的概念,所以原则上在EVM中可执行的计算总量受Gas总量限制。 +* EVM是一个隔离的环境,在EVM内部运行的代码不能跟外部有任何联系,比如屏蔽掉了直接调用系统api。 + + +

2.虚拟机设计组成

+ +* 编译器 + * 多语言支持 + * 新设计一门语言 +* 虚拟机 + * 指令 opcode算数相关 + * 指令 INSTRUCTION + +* 执行机 + * 栈 + * 内存 + * statedb + * 执行过程 + * 解释器 + +

3.evm设计目标

+ +* 简单:操作码尽可能的少并且低级,数据类型尽可能少,虚拟机的结构尽可能少; +* 结果明确:在VM规范语句中,没有任何可能产生歧义的空间,结果应该是完全确定的。此外,计算步骤应该是精确的,以便可以测量Gas的消耗量; +* 节约空间:EVM组件应尽可能紧凑; +* 预期应用应具备专业化能力:在VM上构建的应用应能处理20字节的地址,以及32位的自定义加密值,拥有用于自定义加密的模数运算、读取区块和交易数据与状态交互等能力; +* 简单安全:为了让VM不被利用,应该能够容易地建立一套Gas消耗成本模型的操作; +* 优化友好:应该易于优化,以便即时编译(JIT)和VM的加速版本能够构建出来。 + + + +

4.evm优缺点

+ +* 优点: + * EVM中要么执行智能合约的所有代码,要么一点也不执行,完全不可能只执行其中部分代码,无论是燃料不足还是无效指令,最终状态恢复到交易执行前的checkpoint,这样可以很大程度规避作恶的可能。程序总是从头开始运行,无法跳过Solidity ABI引导代码。 + * evm与网络,文件系统,进程等隔离 + * 对外是完全隔离的,甚至不同合约之间也只有有限的访问权限 + +* 缺点: + * 机器码长度为256位 + * 缺少标准库 + * 难以调试和测试 + * 不支持浮点数 + * 不可修改的代码 + +

5.IELE设计原理

+ +* IELE是LLVM的一个变种,专门用于在区块链上执行智能合约 +* 与基于栈的EVM不同,IELE是基于寄存器的机器,就像LLVM。它支持无限的寄存器以及无界整数。 +* 合约可以通过ABI(应用程序二进制接口)相互作用 +* 为所有语言提供统一的gas模型。 +* 让编写安全的智能合约更容易。 它避免了使用有界堆栈,而不必担心堆栈或内存泄漏,使智能合约的规范和验证更容易。 + + +

6.evm和其他语言虚拟机区别

+ +* evm和jvm,lvm最大的区别就是缺少任务调度和上下文切换,因为这一部分只涉及到性能并不牵扯到执行安全,所以可以将其抽取到外面,比如以太坊其实是用编程语言自带协程来完成即可,而整个虚拟机可以类似理解成一个请求的上下文,这也说明了evm虚拟机其实只需要保证合约执行安全前提下,越精简越好。 +* 相比较嵌入式设备上的自建安全语言虚拟机,区块链虚拟机另一大特点是开源,无法通过代码扰乱,重构内存模型(比如地址不连续的栈)等技术实现其安全 +* 共同点,相比较lvm虚拟机都可以实现jit加速执行效率,本身分布式架构,这种效率提升又显得并不是很重要 + + +###### 参考链接 +**https://github.com/ethereumbook/ethereumbook/blob/develop/evm.asciidoc** +**https://blog.csdn.net/huhaoxuan2010/article/details/80130942** +**http://www.freebuf.com/column/167227.html** +**https://www.jianshu.com/p/188f6eeb85e3** +***https://www.jianshu.com/p/e45614c25b62?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation*** + + + + + + diff --git a/VM/EVM/image/ExecutiveCallFlowChart.jpg b/VM/EVM/image/ExecutiveCallFlowChart.jpg new file mode 100644 index 0000000..b4b8d13 Binary files /dev/null and b/VM/EVM/image/ExecutiveCallFlowChart.jpg differ diff --git a/VM/EVM/image/ExecutiveCreateFlowChart.jpg b/VM/EVM/image/ExecutiveCreateFlowChart.jpg new file mode 100644 index 0000000..94c49c6 Binary files /dev/null and b/VM/EVM/image/ExecutiveCreateFlowChart.jpg differ diff --git a/VM/EVM/image/ExecutiveGoFlowchart.jpg b/VM/EVM/image/ExecutiveGoFlowchart.jpg new file mode 100644 index 0000000..77fc74d Binary files /dev/null and b/VM/EVM/image/ExecutiveGoFlowchart.jpg differ diff --git a/VM/EVM/image/MainClass.jpg b/VM/EVM/image/MainClass.jpg new file mode 100644 index 0000000..3a86bab Binary files /dev/null and b/VM/EVM/image/MainClass.jpg differ diff --git "a/VM/evm-ewasm/EOS\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" "b/VM/evm-ewasm/EOS\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" new file mode 100644 index 0000000..af27b54 --- /dev/null +++ "b/VM/evm-ewasm/EOS\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" @@ -0,0 +1,543 @@ +# EOS对WASM的支持 + +## 一、EOS为什么选择WASM +EOS为什么选择在虚拟机中使用WASM,主要的原因就是: +
+1、支持C/C++等高级语言,效率高。 +
+2、由于多语言的兼容性导致学习的成本大大降低。基本所有语言的人都可以写智能合约。 +
+3、谷歌、苹果、微软等大公司的强有力支持。 +
+4、既支持解释型虚拟机又支持直接编译成机器码执行,既考虑了性能又兼顾了兼容性。 +
+5、WASM在持续迭代中,而EOS也在持续迭代中。所以影响较小。 +
+通过上面的几点,可以看出,EOS其实最主要还是从效率和生态来考虑的,选择WASM,意味着开发这一块的生态,基本已经成型,不用考虑再引进大量的工作对其进行建设,对比以太坊的SOLIDITY,就可以看出来它的优势所在。 + +## 二、eos的合约例程编译 +EOS的智能合约是用c++开发的(当然,目前也出现了很多其它语言的版本),通过LLVM来进行编译生成WASM。下面先看一个例子: +
+ +```C++ +#include + +using namespace eosio; + +CONTRACT hello : public eosio::contract { + public: + using contract::contract; + + ACTION hi( name user ) { + print( "Hello, ", name{user} ); + } +}; + +EOSIO_DISPATCH( hello, (hi) ) + +//wasm +"0061736d01000000013e0c60027......0b0438200000" +//wast +(module +(type (;0;) (func (param i32 i64))) +(type (;1;) (func (param i32 i32))) +(type (;2;) (func (param i32 i32 i32) (result i32))) +(type (;3;) (func (result i32))) +(type (;4;) (func (param i32 i32) (result i32))) +(type (;5;) (func (param i32))) +(type (;6;) (func (param i64))) +(type (;7;) (func)) +(type (;8;) (func (param i32) (result i32))) +(type (;9;) (func (param i64 i64 i64))) +(type (;10;) (func (param i64 i64 i32) (result i32))) +(type (;11;) (func (param i64 i64))) +(import "env" "eosio_assert" (func (;0;) (type 1))) +(import "env" "memset" (func (;1;) (type 2))) +(import "env" "action_data_size" (func (;2;) (type 3))) +(import "env" "read_action_data" (func (;3;) (type 4))) +(import "env" "memcpy" (func (;4;) (type 2))) +(import "env" "prints" (func (;5;) (type 5))) +(import "env" "printn" (func (;6;) (type 6))) +(import "env" "eosio_assert_code" (func (;7;) (type 0))) +(func (;8;) (type 7) +call 11) +(func (;9;) (type 8) (param i32) (result i32) +(local i32 i32 i32) +block ;; label = @1 + block ;; label = @2 + block ;; label = @3 + block ;; label = @4 + get_local 0 + i32.eqz + br_if 0 (;@4;) + i32.const 0 + i32.const 0 + i32.load offset=8204 + get_local 0 + i32.const 16 + i32.shr_u + tee_local 1 + i32.add + tee_local 2 + i32.store offset=8204 + i32.const 0 + i32.const 0 + i32.load offset=8196 + tee_local 3 + get_local 0 + i32.add + i32.const 7 + i32.add + i32.const -8 + i32.and + tee_local 0 + i32.store offset=8196 + get_local 2 + i32.const 16 + i32.shl + get_local 0 + i32.le_u + br_if 1 (;@3;) + get_local 1 + memory.grow + i32.const -1 + i32.eq + br_if 2 (;@2;) + br 3 (;@1;) + end + i32.const 0 + return + end + i32.const 0 + get_local 2 + i32.const 1 + i32.add + i32.store offset=8204 + get_local 1 + i32.const 1 + i32.add + memory.grow + i32.const -1 + i32.ne + br_if 1 (;@1;) + end + i32.const 0 + i32.const 8208 + call 0 + get_local 3 + return +end +get_local 3) +(func (;10;) (type 5) (param i32)) +(func (;11;) (type 7) +...... +i32.add +set_global 0) +(table (;0;) 2 2 anyfunc) +(memory (;0;) 1) +(global (;0;) (mut i32) (i32.const 8192)) +(global (;1;) i32 (i32.const 8246)) +(global (;2;) i32 (i32.const 8246)) +(export "apply" (func 13)) +(elem (i32.const 1) 14) +(data (i32.const 8208) "failed to allocate pages\00Hello, \00") +(data (i32.const 8241) "read\00") +(data (i32.const 0) "8 \00\00")) +``` +
+一个标准的入门的智能合约,后面的编译结果由于篇幅太长,省略了大部分。下面提供了几个在线的EOS智能合约IDE: +
+https://beosin.com/BEOSIN-IDE/index.html#/ +
+https://tbfleming.github.io/cib/eos-slim.html +
+https://app.eosstudio.io/ +
+在EOS虚拟机中,其实就是对上述编译结果的执行过程,如果大家有过解释型虚拟机的经验理解起来就很容易了。下面针对这个例子,来分析一下JIT,在前面的Webassembly中,说明了,LLVM做为一种新的编译器,它采用的与传统的编译器的方式不同,LLVM采用了分段式编译,提供了一个层中间代码IR,搭起了前端到后端的桥梁,同样,也正是采用了这种机制,使得LLVM的兼容性和适应性大大提高。 +在Webassembly中正是借鉴了这种方式,也提出了一种更低级的中间代码BYTECODE(字节码),通过字节码来实现更好的适应性。看一下上面的代码片段: +
+ +```C++ +(func (;9;) (type 8) (param i32) (result i32) +(local i32 i32 i32) +``` +
+在代码中,可以通过get_local得到局部变量,通过get_global得到全局变量,后面跟0(即get_local 0)表示参数的索引序列,比如上面的例子就可以i32,1表示i32...依次类推。这个和汇编语言中拿取参数有些类似。 +
+从上面的wast中,可以到前面分析的Webassembly中的各种数据结构和接口。如果有兴趣,可以结合着官方的文档仔细的分析一下,这些东西都是固定的,没有什么技术可言,这里就不再展开分析。 +
+如果使用的编译器只提供了二进制的代码,不好分析的话,可以使用提供的工具集WABT(里面很多相关的处理工具)来处理一下,举一个例子: +
+用来查看类似反编译的详细内容: +
+./wat2wasm firstExample.wat -v +
+用来将wast转换成wasm: +
+wat2wasm firstExample.wast -o firstExample.wasm +
+更多的功能,请查看此工具集的具体的应用方法,地址在: +
+https://github.com/WebAssembly/wabt/ +
+接下来分析一下EOS的虚拟机。 + +## 三、eos虚拟机wasm-jit +虚拟机的主要代码在libraries/wasm-jit/Source目录下,目前EOS虚拟对上面提到的WASM和WAST两种格式都是支持的。因为EOS的更新速度太快,所以有些说明可能就过时了,大家如果发现这种情况,请及时跟上EOSIO的官网即可。目前来看,EOS的虚拟有两块,一块是WAVM版本: +
+https://github.com/EOSIO/WAVM +
+一块是新独立出来的EOS-VM部分: +
+https://github.com/EOSIO/eos-vm +
+因为第二部分还不敢确定到底有没有应用到EOS上,所以暂时以第一部分分析。EOS选择WASM是出于综合的考虑的。虽然说完全套用LLVM会更简单,但这会有一个问题,就是直接和LLVM绑定。这一定不是EOS设计者们考虑问题的结果。所以,只能是牺牲一下效率,达到所谓的平衡。 +
+先看一下虚拟机暴露的接口: +
+ +```C++ +namespace eosio { namespace chain { + +class apply_context; + +class wasm_instantiated_module_interface { + public: + virtual void apply(apply_context& context) = 0; + + virtual ~wasm_instantiated_module_interface(); +}; + +class wasm_runtime_interface { + public: + virtual std::unique_ptr instantiate_module(const char* code_bytes, size_t code_size, std::vector initial_memory) = 0; + + //immediately exit the currently running wasm_instantiated_module_interface. Yep, this assumes only one can possibly run at a time. + virtual void immediately_exit_currently_running_module() = 0; + + virtual ~wasm_runtime_interface(); +}; + +}} + +//其下为实现 +//创建一个unique_ptr的独占实例指针 +std::unique_ptr wavm_runtime::instantiate_module(const char* code_bytes, size_t code_size, std::vector initial_memory) { + std::unique_ptr module = std::make_unique(); + try { + Serialization::MemoryInputStream stream((const U8*)code_bytes, code_size); + WASM::serialize(stream, *module); + } catch(const Serialization::FatalSerializationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } catch(const IR::ValidationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); + } + + eosio::chain::webassembly::common::root_resolver resolver; + LinkResult link_result = linkModule(*module, resolver); + ModuleInstance \*instance = instantiateModule(*module, std::move(link_result.resolvedImports)); + EOS_ASSERT(instance != nullptr, wasm_exception, "Fail to Instantiate WAVM Module"); + + return std::make_unique(instance, std::move(module), initial_memory); +} +//实现Apply +void apply(apply_context& context) override { + vector args = {Value(uint64_t(context.get_receiver())), + Value(uint64_t(context.get_action().account)), + Value(uint64_t(context.get_action().name))}; + + call("apply", args, context); +} + +private: +void call(const string &entry_point, const vector &args, apply_context &context) { + try { + FunctionInstance* call = asFunctionNullable(getInstanceExport(\_instance,entry_point)); + if( !call ) + return; + + EOS_ASSERT( getFunctionType(call)->parameters.size() == args.size(), wasm_exception, "" ); + + MemoryInstance* default_mem = getDefaultMemory(\_instance); + if(default_mem) { + //reset memory resizes the sandbox'ed memory to the module's init memory size and then + // (effectively) memzeros it all + resetMemory(default_mem, \_initial_memory_config); + + char* memstart = &memoryRef(getDefaultMemory(\_instance), 0); + memcpy(memstart, \_initial_memory.data(), \_initial_memory.size()); + } + + the_running_instance_context.memory = default_mem; + the_running_instance_context.apply_ctx = &context; + + resetGlobalInstances(\_instance); + runInstanceStartFunc(\_instance); + Runtime::invokeFunction(call,args); + } catch( const wasm_exit& e ) { + } catch( const Runtime::Exception& e ) { + FC_THROW_EXCEPTION(wasm_execution_error, + "cause: ${cause}\n${callstack}", + ("cause", string(describeExceptionCause(e.cause))) + ("callstack", e.callStack)); + } FC_CAPTURE_AND_RETHROW() +} +``` +接口很简单,只有三个函数,apply、instantiate_module和immediately_exit_currently_running_module。在EOS的智能合约中apply接口是必须实现的(通过EOSIO_ABI宏来实现)。程序的两个主要接口首先产生个接口实例的独占指针。然后在apply中调用call函数。在apply中,会得到三个相关的参数,即代码,帐户和action的名称。 +
+在call函数中,首先得到call函数指针,通过MemoryInstance指针得到公用的WASM模块的内存实例。将其重置并初始化,绑定到相关的上下文信息中。重置相关的全局变量。然后调用模块的起始函数(这个在前面WASM中介绍过,可有可无,根据实际情况来定),接着调用EOB_ABI的apply函数。这样整个的虚拟机的核心流程就清楚了。 +执行时是要生成IR中间语言来处理,看一下IR的部分: +
+ +```C++ +IR::Module module; +try { + Serialization::MemoryInputStream stream((const U8*)codeobject->code.data(), codeobject->code.size()); + WASM::serialize(stream, module); + module.userSections.clear(); +} catch(const Serialization::FatalSerializationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); +} catch(const IR::ValidationException& e) { + EOS_ASSERT(false, wasm_serialization_error, e.message.c_str()); +} + +//WASMSerializatin.cpp中有很多的重载实现 +void serializeModule(InputStream& moduleStream,Module& module) +{ + serializeConstant(moduleStream,"magic number",U32(magicNumber)); + serializeConstant(moduleStream,"version",U32(currentVersion)); + + SectionType lastKnownSectionType = SectionType::unknown; + while(moduleStream.capacity()) + { + const SectionType sectionType = *(SectionType*)moduleStream.peek(sizeof(SectionType)); + if(sectionType != SectionType::user) + { + if(sectionType > lastKnownSectionType) { lastKnownSectionType = sectionType; } + else { throw FatalSerializationException("incorrect order for known section"); } + } + switch(sectionType) + { + case SectionType::type: serializeTypeSection(moduleStream,module); break; + case SectionType::import: serializeImportSection(moduleStream,module); break; + case SectionType::functionDeclarations: serializeFunctionSection(moduleStream,module); break; + case SectionType::table: serializeTableSection(moduleStream,module); break; + case SectionType::memory: serializeMemorySection(moduleStream,module); break; + case SectionType::global: serializeGlobalSection(moduleStream,module); break; + case SectionType::export_: serializeExportSection(moduleStream,module); break; + case SectionType::start: serializeStartSection(moduleStream,module); break; + case SectionType::elem: serializeElementSection(moduleStream,module); break; + case SectionType::functionDefinitions: serializeCodeSection(moduleStream,module); break; + case SectionType::data: serializeDataSection(moduleStream,module); break; + case SectionType::user: + { + UserSection& userSection = \*module.userSections.insert(module.userSections.end(),UserSection()); + serialize(moduleStream,userSection); + break; + } + default: throw FatalSerializationException("unknown section ID"); + }; + }; +} + +``` +
+到这里就应该明白怎么做了吧。其实就是对具体的Webassmbly的不同的节进行不同的处理。更具体的代码请关注EOS的相关源码。 +
+智能合约的整体流程,包括部署、调用、存储和执行。执行的核心部分前边分析了,部署、存储和虚拟机不是很紧密,下来看一下调用: +
+调用,在EOS中,交易的分发是通过void transaction_context::execute_action这个函数来处理action的(交易如何最终传到此处,可以查看代码相关部分),再调用exec,直到exec_one: +
+ +```C++ +{ + receiver_account = &db.get( receiver ); + privileged = receiver_account->is_privileged(); + auto native = control.find_apply_handler( receiver, act->account, act->name ); + if( native ) { + if( trx_context.enforce_whiteblacklist && control.is_producing_block() ) { + control.check_contract_list( receiver ); + control.check_action_list( act->account, act->name ); + } + (\*native)( \*this ); + } + + if( ( receiver_account->code_hash != digest_type() ) && + ( !( act->account == config::system_account_name + && act->name == N( setcode ) + && receiver == config::system_account_name ) + || control.is_builtin_activated( builtin_protocol_feature_t::forward_setcode ) + ) + ) { + if( trx_context.enforce_whiteblacklist && control.is_producing_block() ) { + control.check_contract_list( receiver ); + control.check_action_list( act->account, act->name ); + } + try { + control.get_wasm_interface().apply( receiver_account->code_hash, receiver_account->vm_type, receiver_account->vm_version, *this ); + } catch( const wasm_exit& ) {} + } +``` +
+在合约的调用过程中,分成两种情况,即本地(系统)合约,像发币的合约,同样也有自己部署的合约。这里看后者,注意最后的apply的调用。而apply函数是通过设置Handler来实现的: +
+ +```C++ +#define SET_APP_HANDLER( receiver, contract, action) \ + set_apply_handler( #receiver, #contract, #action, &BOOST_PP_CAT(apply_, BOOST_PP_CAT(contract, BOOST_PP_CAT(_,action) ) ) ) + + SET_APP_HANDLER( eosio, eosio, newaccount ); + SET_APP_HANDLER( eosio, eosio, setcode ); + SET_APP_HANDLER( eosio, eosio, setabi ); + SET_APP_HANDLER( eosio, eosio, updateauth ); + SET_APP_HANDLER( eosio, eosio, deleteauth ); + SET_APP_HANDLER( eosio, eosio, linkauth ); + SET_APP_HANDLER( eosio, eosio, unlinkauth ); +/* + SET_APP_HANDLER( eosio, eosio, postrecovery ); + SET_APP_HANDLER( eosio, eosio, passrecovery ); + SET_APP_HANDLER( eosio, eosio, vetorecovery ); +*/ + + SET_APP_HANDLER( eosio, eosio, canceldelay ); + } +//看find_apply_handler的对应注册机制 +void set_apply_handler( account_name receiver, account_name contract, action_name action, apply_handler v ) { + apply_handlers[receiver][make_pair(contract,action)] = v; +} +``` +
+继续看apply: +
+ +```C++ +void wasm_interface::apply( const digest_type& code_hash, const uint8_t& vm_type, const uint8_t& vm_version, apply_context& context ) { + my->get_instantiated_module(code_hash, vm_type, vm_version, context.trx_context)->apply(context); +} +``` +
+这下就明白了,调用init的模块,然后再调用相关的apply.这其实就是一个查询相关的合约实例,然后再通过此实例调用其自身的apply.一路下来真的好绕口。从执行action,到查找注册的handler,执行系统合约,通过apply执行部署的合约。 +
+再来看一下WAVM的解释器(Binaryen的与其类似),前面提到过三个接口,其中一个就是生成实例的,代码最后: +
+ +```C++ +std::unique_ptr wavm_runtime::instantiate_module(const char* code_bytes, size_t code_size, std::vector initial_memory) { + std::unique_ptr module = std::make_unique(); +...... + return std::make_unique(instance, std::move(module), initial_memory); +} +//会调用 +ModuleInstance* instantiateModule(const IR::Module& module,ImportBindings&& imports) +{ + ...... + // Generate machine code for the module. + //这里调用LLVMJIT,为后来编译提供资源 + LLVMJIT::instantiateModule(module,moduleInstance); + + // Set up the instance's exports. + for(const Export& exportIt : module.exports) + { + ObjectInstance* exportedObject = nullptr; + switch(exportIt.kind) + { + case ObjectKind::function: exportedObject = moduleInstance->functions[exportIt.index]; break; + case ObjectKind::table: exportedObject = moduleInstance->tables[exportIt.index]; break; + case ObjectKind::memory: exportedObject = moduleInstance->memories[exportIt.index]; break; + case ObjectKind::global: exportedObject = moduleInstance->globals[exportIt.index]; break; + default: Errors::unreachable(); + } + moduleInstance->exportMap[exportIt.name] = exportedObject; + } +...... +} +void instantiateModule(const IR::Module& module,ModuleInstance* moduleInstance) +{ + // Emit LLVM IR for the module. + auto llvmModule = emitModule(module,moduleInstance); + + // Construct the JIT compilation pipeline for this module. + auto jitModule = new JITModule(moduleInstance); + moduleInstance->jitModule = jitModule; + + // Compile the module. + jitModule->compile(llvmModule); +} +``` +
+看到最后一行没有,其实是调用的LLVMJIT的编译方法,编译完成后,开始对apply的处理,这个函数在前面核心流程时提到过: +
+ +```C++ +void call(const string &entry_point, const vector &args, apply_context &context) { + try { + FunctionInstance* call = asFunctionNullable(getInstanceExport(\_instance,entry_point)); + ...... + Runtime::invokeFunction(call,args); + } +...... +} +``` +
+这里重点看第一行和最后一行,调用的函数: +
+ +```C++ +Result invokeFunction(FunctionInstance* function,const std::vector& parameters) +{ + const FunctionType* functionType = function->type; + + // Check that the parameter types match the function, and copy them into a memory block that stores each as a 64-bit value. + //参数检测 + if(parameters.size() != functionType->parameters.size()) + { + throw Exception {Exception::Cause::invokeSignatureMismatch}; + } + + //分配主要的内存——参数的大小+返回值大小 + U64* thunkMemory = (U64*)alloca((functionType->parameters.size() + getArity(functionType->ret)) * sizeof(U64)); + //参数的安全检测 + for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size();++parameterIndex) + { + if(functionType->parameters[parameterIndex] != parameters[parameterIndex].type) + { + throw Exception {Exception::Cause::invokeSignatureMismatch}; + } + + thunkMemory[parameterIndex] = parameters[parameterIndex].i64; + } + + // Get the invoke thunk for this function type. + //获得执行函数指针 + LLVMJIT::InvokeFunctionPointer invokeFunctionPointer = LLVMJIT::getInvokeThunk(functionType); + + // Catch platform-specific runtime exceptions and turn them into Runtime::Values. + Result result; + Platform::HardwareTrapType trapType; + Platform::CallStack trapCallStack; + Uptr trapOperand; + //下面这一大段LAMBADA表达式类似于CallBack的调用 + trapType = Platform::catchHardwareTraps(trapCallStack,trapOperand, + [&] + { + // Call the invoke thunk. + (\*invokeFunctionPointer)(function->nativeFunction,thunkMemory); + + // Read the return value out of the thunk memory block. + if(functionType->ret != ResultType::none) + { + result.type = functionType->ret; + result.i64 = thunkMemory[functionType->parameters.size()]; + } + }); + + // If there was no hardware trap, just return the result. + if(trapType == Platform::HardwareTrapType::none) { return result; } + else { handleHardwareTrap(trapType,std::move(trapCallStack),trapOperand); } +} +``` +
+这样,一个WAVM的执行过程就基本完成了。再到深处,可以参考LLVMJIT的实现机制,这里就不再展开,有兴趣的可以参考相关官网或者开发者文档。 + +## 四、总结 +这里主要从调用和执行两条线进行了分析,同时辅以了IR的生成的分析过程。EOS的虚拟机的迭代速度应该和EOS本身一样,不断的变化的着,但是如果不迭代到EOS-VM,则变化就不会有颠覆性的。以后有机会仔细分析一下EOS-VM,这个以头文件形式形成的虚拟机的库,据说执行速度提高了很多倍。 diff --git a/VM/evm-ewasm/img/emvc-struct.png b/VM/evm-ewasm/img/emvc-struct.png new file mode 100644 index 0000000..f299f4b Binary files /dev/null and b/VM/evm-ewasm/img/emvc-struct.png differ diff --git a/VM/evm-ewasm/img/hera-struct.png b/VM/evm-ewasm/img/hera-struct.png new file mode 100644 index 0000000..6b92bde Binary files /dev/null and b/VM/evm-ewasm/img/hera-struct.png differ diff --git a/VM/evm-ewasm/img/module.jpg b/VM/evm-ewasm/img/module.jpg new file mode 100644 index 0000000..1d9253d Binary files /dev/null and b/VM/evm-ewasm/img/module.jpg differ diff --git a/VM/evm-ewasm/img/wasm-api.png b/VM/evm-ewasm/img/wasm-api.png new file mode 100644 index 0000000..c499bb9 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-api.png differ diff --git a/VM/evm-ewasm/img/wasm-datatype.png b/VM/evm-ewasm/img/wasm-datatype.png new file mode 100644 index 0000000..71c6469 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-datatype.png differ diff --git a/VM/evm-ewasm/img/wasm-file.png b/VM/evm-ewasm/img/wasm-file.png new file mode 100644 index 0000000..cf15560 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-file.png differ diff --git a/VM/evm-ewasm/img/wasm-header.png b/VM/evm-ewasm/img/wasm-header.png new file mode 100644 index 0000000..9c9b190 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-header.png differ diff --git a/VM/evm-ewasm/img/wasm-hex.png b/VM/evm-ewasm/img/wasm-hex.png new file mode 100644 index 0000000..9328434 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-hex.png differ diff --git a/VM/evm-ewasm/img/wasm-scode.png b/VM/evm-ewasm/img/wasm-scode.png new file mode 100644 index 0000000..50c6392 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-scode.png differ diff --git a/VM/evm-ewasm/img/wasm-scustom.png b/VM/evm-ewasm/img/wasm-scustom.png new file mode 100644 index 0000000..1bf40e3 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-scustom.png differ diff --git a/VM/evm-ewasm/img/wasm-sdata.png b/VM/evm-ewasm/img/wasm-sdata.png new file mode 100644 index 0000000..69526c0 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sdata.png differ diff --git a/VM/evm-ewasm/img/wasm-sectiontype.png b/VM/evm-ewasm/img/wasm-sectiontype.png new file mode 100644 index 0000000..a47e49d Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sectiontype.png differ diff --git a/VM/evm-ewasm/img/wasm-selem.png b/VM/evm-ewasm/img/wasm-selem.png new file mode 100644 index 0000000..19e19f2 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-selem.png differ diff --git a/VM/evm-ewasm/img/wasm-sexport.png b/VM/evm-ewasm/img/wasm-sexport.png new file mode 100644 index 0000000..9f73522 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sexport.png differ diff --git a/VM/evm-ewasm/img/wasm-sfunc.png b/VM/evm-ewasm/img/wasm-sfunc.png new file mode 100644 index 0000000..6f99215 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sfunc.png differ diff --git a/VM/evm-ewasm/img/wasm-sglobal.png b/VM/evm-ewasm/img/wasm-sglobal.png new file mode 100644 index 0000000..cfffbba Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sglobal.png differ diff --git a/VM/evm-ewasm/img/wasm-simport.png b/VM/evm-ewasm/img/wasm-simport.png new file mode 100644 index 0000000..4078de5 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-simport.png differ diff --git a/VM/evm-ewasm/img/wasm-smem.png b/VM/evm-ewasm/img/wasm-smem.png new file mode 100644 index 0000000..2f03307 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-smem.png differ diff --git a/VM/evm-ewasm/img/wasm-smem1.png b/VM/evm-ewasm/img/wasm-smem1.png new file mode 100644 index 0000000..3f266c2 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-smem1.png differ diff --git a/VM/evm-ewasm/img/wasm-sresizable_limits.png b/VM/evm-ewasm/img/wasm-sresizable_limits.png new file mode 100644 index 0000000..ce08afd Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sresizable_limits.png differ diff --git a/VM/evm-ewasm/img/wasm-sstart.png b/VM/evm-ewasm/img/wasm-sstart.png new file mode 100644 index 0000000..a9d497f Binary files /dev/null and b/VM/evm-ewasm/img/wasm-sstart.png differ diff --git a/VM/evm-ewasm/img/wasm-stable.png b/VM/evm-ewasm/img/wasm-stable.png new file mode 100644 index 0000000..5c7ccd1 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-stable.png differ diff --git a/VM/evm-ewasm/img/wasm-stable1.png b/VM/evm-ewasm/img/wasm-stable1.png new file mode 100644 index 0000000..6abc903 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-stable1.png differ diff --git a/VM/evm-ewasm/img/wasm-stype.png b/VM/evm-ewasm/img/wasm-stype.png new file mode 100644 index 0000000..499ba2a Binary files /dev/null and b/VM/evm-ewasm/img/wasm-stype.png differ diff --git a/VM/evm-ewasm/img/wasm-stype1.png b/VM/evm-ewasm/img/wasm-stype1.png new file mode 100644 index 0000000..ee5a533 Binary files /dev/null and b/VM/evm-ewasm/img/wasm-stype1.png differ diff --git "a/VM/evm-ewasm/wasm\346\226\207\344\273\266\346\240\274\345\274\217.md" "b/VM/evm-ewasm/wasm\346\226\207\344\273\266\346\240\274\345\274\217.md" new file mode 100644 index 0000000..3828519 --- /dev/null +++ "b/VM/evm-ewasm/wasm\346\226\207\344\273\266\346\240\274\345\274\217.md" @@ -0,0 +1,244 @@ +# wasm文件格式 + +# 一、模块 +在WASM的文件组织上,既不像传统的程序编写过程中存在执行文件和库文件的区别,也不像某些编译器提供的中间状态文件。其提供了一种脱离于具体的物理架构的“虚拟的指令体系”,或者如果一定要和其它一些固有的形式来绑定的话,它更倾向于一种“虚拟机格式”。 +
+一般来说,都是对应用目标进行高级的抽象,但在WASM中,可以认为是一种的抽象,它是面向编译器的,而不是面向程序员的。 +
+WASM是基于模块的,或者说,每个程序段都可以自由的组成模块,每个模块都可以有自己的主入口,相当于main函数。模块是整个Webassembly代码中的基本单元,在前面提到过,它有两种格式,即wat,wasm两种格式,在wast文件中,它相当于一个S-表达式。 +
+S-表达式的优点在于它是可以用人类的语言来看明白的,对人是友好的。其实它也是在图论中的剑桥波兰表达式法。它的基本表现方法如下: +
+ +``` +(module (memory 1)(func)) +``` +
+回忆一下上一篇文章中的WAT文件: +
+ +```C++ +(module + (type $t0 (func)) + ...... +) +``` +一个合法的模块,是以module开始的,用UE(或者其它的十进制编辑器)打开前文中的wasm文件: +
+ +![wasmh](img/wasm-header.png) +
+红色杠住的二进制代码:0061 736d是所有文件格式都有的标志:文件的魔数。 +
+蓝色框住的二进制代码:0100 0000 是wasm文件的版本号。 +
+在前面的文档中提到了,wast和 wasm两种文件可以通过工具进行转换。 +
+ +# 二、文件组成 +现在重点分析一下wasm的文件格式: +
+ +![wasmfile](img/wasm-file.png) +
+对比上面的文件格式,看一下实际的格式: +
+ +![wasmhex](img/wasm-hex.png) +
+再对比一下上文中的wast文件: +
+ +```C++ +(module + (type $t0 (func)) + (type $t1 (func (param i32 i32) (result i32))) + (type $t2 (func (result i32))) + (func $\__wasm_call_ctors (type $t0)) + (func $add (export "add") (type $t1) (param $p0 i32) (param $p1 i32) (result i32) + get_local $p1 + get_local $p0 + i32.add) + (func $main (export "main") (type $t2) (result i32) + i32.const 42) + (table $T0 1 1 anyfunc) + (memory $memory (export "memory") 2) + (global $g0 (mut i32) (i32.const 66560)) + (global $\__heap_base (export "\__heap_base") i32 (i32.const 66560)) + (global $\__data_end (export "\__data_end") i32 (i32.const 1024))) +``` +### 1、Section Type种类 +Type的类型目前只有12种: +
+ +![type](img/wasm-sectiontype.png) +
+它们的编码,依次排序下去。其在WA的官网的文件中Module中可以推导出以下的形式: +
+ +``` +module::= +{ + types vec(functype), + funcs vec(func), + tables vec(table), + mems vec(mem), + globals vec(global), + elem vec(elem), + data vec(data), + start start?, + imports vec(import), + exports vec(export)} +``` +
+下面分别介绍一下各个节的内容: +
+ +1)Custom Section: +
+这个提供了一个自定义机制,允许在此处增加调试信息或者相关的扩展,它会被Webassembly的主义忽略。其形式如下: + +![s0](img/wasm-scustom.png) +
+2) Type section +
+ +![s1](img/wasm-stype.png) +
+![s1-1](img/wasm-stype1.png) +
+类型区提供了一组函数签名,在其前面还有一个数组的长度,在上面的定义里可以看到函数的第一个字段固定为0x60.需要注意的是,目前函数的返回值只支持一个。举一个例子:int test(int x,int y),其函数签名为:0x60 2 i32 i32 1 i32。即上面描述的,固定标记0x60,加上参数类型个数和参数类型,加上返回值个数和返回值类型。 +
+3) import section +
+ +![s2](img/wasm-simport.png) +
+导入区就是从其它模块导入的相关的函数、表等。导入区也是数组大小+数组。内容包括:导入的模块名称、字段名称、导入类型及相关信息。其中0表示函数,后面是函数签名在数组中的索引;1表示表,后面表示表中的元素类型及表的容量大小等;2表示内存,后面是初始和最大容量,单位是页。每页为64K;3表示全局变量,其后是类型和是否只读。 +
+4) function section +
+ +![s4](img/wasm-sfunc.png) +
+函数区和代码区的函数代码一一对应。它指向类型区中的函数签名数组中的某个签名的索引。 +
+5) table section +
+ +![s5](img/wasm-stable.png) +
+ +![s5-1](img/wasm-stable1.png) +
+表中主要存储了函数的索引的列表。用来供运行进调用指向的函数。专门提供一个表而不是放到内存中,是考虑到安全的因素。 +
+6) memory section +
+ +![s6](img/wasm-smem.png) +
+![s6-1](img/wasm-smem1.png) +
+内存区是存储模块运行所需要的内存空间的,在模块初始化时由数据区进行填充。不过这里面因为不能确定类型,所以只能以字节形式来处理,这和其它汇编表示一样。 +
+7) global section +
+ +![s7](img/wasm-sglobal.png) +
+全局区定义了模块内的全局变量及相关属性信息,包括只读等,它的结构也是数组长度+数组。
+8) export section +
+ +![s8](img/wasm-sexport.png) +
+导出和导入正好相反,这个其它语言中库的导出标记一样,如果没有导出,则外部不可以引用。同样,其也是一个数组长度+数组。其内容和导入基本一致。 +
+9) start section +
+ +![s9](img/wasm-sstart.png) +
+开始区有点类似于main函数,或者如果有汇编编程经验中的start.s文件,它只有一个字段,当模块加载完成后,如果有这个区,是会首先调用其内部指向的函数。 +
+10) element section +
+ +![s10](img/wasm-selem.png) +
+元素区同样是一个数组,它用来填充表区,主要包括表索引、表中的偏移值和指向函数索引。 +
+11) code section +
+ +![s11](img/wasm-scode.png) +
+代码区定义了模块中的函数定义,它和函数区内的函数签名一一对应。它也是由数组长度+数组组成。在每个数组单元内,描述了函数体的大小(字节数)、局部变量、代码。代码是由具体的指令集中的指令组成来完成函数要实现的具体的功能。 +
+12) data section +
+ +![s12](img/wasm-sdata.png) +
+数据区同样也是一个数组(长度+数组内容),它定义了如何填充内存区数据结构。每个data_entry表示一个具体的一段内存。 +
+ +### 2、LEB128编码 +先介绍一下LEB128这种数据压缩格式,谷歌为了省点空间,在安卓中用了这个压缩标准,其原理就是一个4字节32位的数字,一般很少占满,比如数字1,2等,它其实用几位就可以表示。下面看看它的工作原理: +
+1)忽略高字节的正负代表位。 +
+2)每七位组成一组,待编码。 +
+3)有符号和无符号的分成不同的情况来处理。 +
+4)无符号,最高位为0表示结束,1表示还有后续字节。待编码的七位为数据。 +
+5)有符号的处理,同上,但需要处理高位为0的不同情况。 +
+6)有符号的处理,同上,需要处理高位为1的不同情况。 +
+从上面可以看出无符号比较简单,有符号还需要处理几种情况,这里举一个无符号的例子,简单理解一下,更多请查阅相关资料: +
+编码: +
+十六进制数字:0x238 ========> 二进制 0000 0010 0011 1000 +
+分成两个七位的组:0000100(高位), 0111000(低位) +
+0000100后面没数据了,所以前面加一个0;0111000后面还有一个字节,所以高为为1,则变为: +
+00000100(即:0x04),10111000(0x70) +
+则小端存放的数据为:0x70,0x04 +
+解码: +
+正好相反,把数据拆开,按编码逆向过程即可。 +
+ +### 3、基础说明 +1)Webassemble数据类型有四种即: +
+i32:32位整数 +
+i64:64位整数 +
+f32:32位浮点数 +
+f64:64位浮点数 +
+2)Webassembly的函数签名:其函数签名由参数类型和返回值类型组成,即: +
+(pars1_type,pars2_type...parsN_type)(result_type),即: +
+functype::=[vec(valtype)]→[vec(valtype)] +
+3)索引空间空间说明: +
+函数、表、内存和全局变量都拥有自己的索引空间,用来记录顺序和位置,供指令调用。本地的索引空间只能在函数内部访问。 +
+ +# 三、总结 +通过上述的分析可以明白,wasm的文件数据组织的格式,那么在以后无论对编译器还是对解释器及其执行的流程,就有一个了清楚的底层认知。 diff --git "a/VM/evm-ewasm/wasm\347\232\204\346\211\247\350\241\214.md" "b/VM/evm-ewasm/wasm\347\232\204\346\211\247\350\241\214.md" new file mode 100644 index 0000000..e396a70 --- /dev/null +++ "b/VM/evm-ewasm/wasm\347\232\204\346\211\247\350\241\214.md" @@ -0,0 +1,27 @@ +# wasm的执行 +## 一、执行的原理 +在网上的资料的介绍中,可以知道,WASM的执行速度要比其它一些JIT的解释型语言高很多。一个最直接的结果就是一些大型游戏和一些复杂的绘图工作都可以在网页上进行了。 +
+为什么它的执行效率这么高呢?在前面提到过,一般JIT都是边解释边编译边优化,如果优化过度还需要反优化,再优化的过程。而WASM则是直接编译成抽象的字节码,只需要在执行过程中由引擎编译成机器码正常执行即可。 + +## 二、执行的方式 +WASM的执行仍然是基于栈的方式,或者说基于栈的虚拟机方式。所谓基于基于栈的虚拟机,就是一个OpCode需要的所有的值,在操作执行之前都已经存放在了堆栈中。由于操作符事先知道自己需要多少值,那么它就可以从栈的顶端弹出相应的数量即可。那么这些指令,就可以相对的来说变得更短,比如说变成一个字节的长度。因为指令不需要对相关的寄存器进行管理,也会相应的减少wasm文件中对寄存器操作的OpCode和相关操作,从而减小wasm的文件。 +
+但是一定要分清楚,这只是虚拟机的部分,真正的当其被加载到浏览器时,浏览器会通过编译来应用实体机的各种寄存器。也正是因为wasm没有指定相应的寄存器,这也给浏览器加载时带来了很大的自由度,由自己的编译器来决定使用哪些寄存器的效率更高。 + +## 三、执行过程 +## 1、WASM的文件获取 +WASM目前主要是在浏览器上应用,所以其执行的第一步是从服务器获得相关的WASM文件,由于其在编译过程中省略了很多的工作,所以其文件相对其它解释型的文件来说要小很多,这对于网络上的数据流动,有着很高的效率。 +## 2、解析 +浏览器拿到WASM文件后,将其转换成一个typed array或arraybuffer. +## 3、编译 +因为在LLVM IR的过程中,相关的优化已经完成,所以此处不需要太多的优化,这就意味着编译的速度非常快。同时,由于WASM的类型固定,编译的过程不需要要解释语言的推导过程,也在另一个方面上提高了效率。 +
+编译器把上面的字节流编译成相关的Module并根据实际情况生成实例并翻译成机器码。 +## 4、执行 +在编译到机器码后,其执行效率可想而知。但是Webassembly的一个缺点是,资源的管理(内存等)目前还是个空白,所以需要手动来控制。这个仁者见仁,不做过多解释。 +
+其在执行过程中还是有不少问题需要完善的,比如WASM对JS中的一些元素的调用的问题等,这都需要进一步的发展。毕竟目前只是MVP的最小功能版本。 + +## 四、总结 +上面简要的分析了Webassembly的执行方式,并将其执行的流程进行了初步的说明。实际上,WASM在执行上还是具有非常大的优势的。也正是因为它的这种抽象机器码的执行格式,使得它的应用前景还是非常不错的。虽然目前其仍有很多需要改进的地方,但从目前发展的速度来看,还是让人有很大的希望的。 diff --git "a/VM/evm-ewasm/wasm\347\232\204\347\233\270\345\205\263\346\216\245\345\217\243.md" "b/VM/evm-ewasm/wasm\347\232\204\347\233\270\345\205\263\346\216\245\345\217\243.md" new file mode 100644 index 0000000..e4ff8fa --- /dev/null +++ "b/VM/evm-ewasm/wasm\347\232\204\347\233\270\345\205\263\346\216\245\345\217\243.md" @@ -0,0 +1,447 @@ +# wasm的相关接口 + +## 一、接口定义 +wasm应用的接口还是比较少的,不过低级到这种地步的抽象,应该也不会有太多的接口,否则就没法弄了,还不如直接操作呢。其接口主要包含以下几个: +
+ +![api](img/wasm-api.png) +
+1、Moudle +
+要想得到一个实例对象,就可以用其构造函数来实现,但需要注意的是,这个只适合于同步使用,同步使用就会有一些限制条件,比如不能太耗时,不能太大等待,在官方的文档中也建议如无必要,请使用compile或者缓存对象的直接读取。其使用的方法如下: +
+ +```js +var Mod = new WebAssembly.Module(bufferSource); +参数: +bufferSource:一个 类型化数组 或 ArrayBuffer,包含要编译的 .wasm 模块的二进制代码。 +``` +
+2、Instance +
+这个接口,浏览器的支持并不完整,可以参看一下相关的支持列表。它主要是得到一个有状态的Module的可执行实例。这个实例包含所有的导出函数,同时允许JS调用这些代码。其是以同步的方式获取实例的。 + +```js +var myInstance = new WebAssembly.Instance(module, importObject); +参数: +module:要被实例化的 WebAssembly.Module 对象 +importObject:一个包含值的对象,导入到新创建的 实例 +返回值: +实例对象。 +``` +
+ +3、Global +
+和其它语言的全局变量类似,可以跨模块访问并被多个模块动态链接。 + +```js +var myGlobal = new WebAssembly.Global(descriptor, value); +参数: +descriptor:有两个属性,一个表示数据类型,一个表示是否可以修改,默认是false +value:变量的值 +``` +
+ +4、instantiate() +
+相对于前面的同步实现,这个是异步机制的。它既提供从数据流式得到实例的方式,也提供从其它编译成功模块中的相关代码来得到实例。 + +```js +Promise WebAssembly.instantiate(bufferSource, importObject); +同上面的instance +``` +
+ +5、instantiateStreaming() +
+在文件系统中现在基本都采用了流式加载,所以这里也不稀奇,这个接口可以从流式底层直接实例化一个模块,而且它非常高效。 +
+ +```js +Promise WebAssembly.instantiateStreaming(source, importObject); +参数: +source:Response或者一个可履行的Promise,也就是编译实例化的源数据。 +importObject :想要导入到新建实例的值的对象。如方法等。 +返回值: +一个Promise,包含两个属性,其中一个是可再实例化的Module,另外一个是实例对象。 +``` +
+6、Memory() +
+创建一个内存对象,其包含一个缓冲数组(arrayBuffer),如果其它语言一样,所有的访问是要通过内存的字节流来实现的。目前一个WASM的内存页面的大小是64K。 + +```js +var myMemory = new WebAssembly.Memory(memoryDescriptor); +参数: +memoryDescriptor:它包含两个属性,一个是内存的初始大小,以页为单位;一个是WASM的内存的最大值(可选项) +异常: +会抛出相关的异常,TypeError或者RangeError。 +``` +
+ +7、Table() +
+创建指定类型大小的Table对象。其类似于数组的结构(细节可看前面的分析),表示存储函数引用的WebAssembly表。它可以由JS或WASM双向访问和更改。注意:表目前只能存储函数引用。 + +```js +var myTable = new WebAssembly.Table(tableDescriptor); +参数: +tableDescriptor:包含三个属性的对象,第一个是元素,目前只能是“anyfunc”,第二个是初始的大小,第三个是最大值(可选项)。 +异常: +同Memory()。 +``` +
+ +8、compile() +
+编译一个二进制代码到一个WASM实例对象。 + +```js +Promise WebAssembly.compile(bufferSource); +参数: +bufferSource:二进制的代码缓冲区。 +返回值: +Promise:一个解析为Module的Promise对象。 +异常: +bufferSource不符合标准, 抛出TypeError 。 +编译异常,会发出一个WebAssembly.CompileError。 +``` +
+ +9、compileStreaming() +
+从数据流中直接编译一个模块。这个前面的流式实例化有些类似。 + +```js +Promise WebAssembly.compileStreaming(source); +参数: +source:一个 Response 对象或一个会履行(fulfill)它的 promise,用来表示你想编译的 .wasm 模块的流式源。 +返回值: +一个 WebAssembly.Module 对象的 Promise。 +异常: +有可能抛出数据源的TypeError异常或者编译异常CompileError。 +``` +
+10、validate() +
+验证二进制代码的合法性。 + +```js +WebAssembly.validate(bufferSource); +参数:略 +异常:略 +返回值: +正确为true,否则false +``` +
+11、compileError() +
+创建一个编译期的错误对象。 + +```js +new WebAssembly.CompileError(message, fileName, lineNumber) +内容同LinkError。 +``` +
+12、LinkError() +
+主要用来构建一个新的链接错误对象,表明在调用Start Function时发生的错误。其方法如下: +
+ +```js +new WebAssembly.LinkError(message, fileName, lineNumber) +参数: +message:错误消息 +fileName:错误引起的文件 +lineNumber:文件中触发异常的代码行号 +``` +
+ +13、RuntimeError() +
+创建一个运行期的错误对象,其方法如下: + +```js +new WebAssembly.RuntimeError(message, fileName, lineNumber) +内容同上LinkError,略过。 +``` +
+ +```html + +WASM + +``` +这时候再回头看前面的例程,就更清楚了,得到代码后,可以用重载的方式得到实例对象(一个直接编译,一个使用异步的实例化),如果是已经编译好的,则导出相关的导出函数。 + +## 二、接口应用 +既然有了接口,那么在应用上就方便了好多,在上面有一个简单的小例子,反复用了很多次,这次来一个比较复杂一些的,代码引自下面的github: +
+https://github.com/cunzaizhuyi/blog-assets/tree/master/WebAssembly + +```js + +``` +
+其调用的wast代码如下: +
+ +``` +(module + (type (;0;) (func (param i32))) + (type (;1;) (func (param i32) (result i32))) + (type (;2;) (func (result i32))) + (type (;3;) (func (param i32 i32))) + (type (;4;) (func (param i32 i32) (result i32))) + (type (;5;) (func)) + (import "env" "DYNAMICTOP_PTR" (global (;0;) i32)) + (import "env" "tempDoublePtr" (global (;1;) i32)) + (import "env" "ABORT" (global (;2;) i32)) + (import "env" "memoryBase" (global (;3;) i32)) + (import "env" "tableBase" (global (;4;) i32)) + (import "global" "NaN" (global (;5;) f64)) + (import "global" "Infinity" (global (;6;) f64)) + (import "env" "abortStackOverflow" (func (;0;) (type 0))) + (import "env" "memory" (memory (;0;) 256)) + (import "env" "table" (table (;0;) 0 anyfunc)) + (func (;1;) (type 1) (param i32) (result i32) + (local i32) + block ;; label = @1 + get_global 10 + set_local 1 + get_global 10 + get_local 0 + i32.add + set_global 10 + get_global 10 + i32.const 15 + i32.add + i32.const -16 + i32.and + set_global 10 + get_global 10 + get_global 11 + i32.ge_s + if ;; label = @2 + get_local 0 + call 0 + end + get_local 1 + return + unreachable + end + unreachable) + (func (;2;) (type 2) (result i32) + get_global 10 + return) + (func (;3;) (type 0) (param i32) + get_local 0 + set_global 10) + (func (;4;) (type 3) (param i32 i32) + block ;; label = @1 + get_local 0 + set_global 10 + get_local 1 + set_global 11 + end) + (func (;5;) (type 3) (param i32 i32) + get_global 12 + i32.const 0 + i32.eq + if ;; label = @1 + get_local 0 + set_global 12 + get_local 1 + set_global 13 + end) + (func (;6;) (type 2) (result i32) + (local i32 i32 i32 i32) + block ;; label = @1 + get_global 10 + set_local 3 + get_global 10 + i32.const 16 + i32.add + set_global 10 + get_global 10 + get_global 11 + i32.ge_s + if ;; label = @2 + i32.const 16 + call 0 + end + i32.const 5 + set_local 1 + get_local 1 + set_local 0 + get_local 3 + set_global 10 + get_local 0 + return + unreachable + end + unreachable) + (func (;7;) (type 4) (param i32 i32) (result i32) + (local i32 i32 i32 i32 i32 i32 i32) + block ;; label = @1 + get_global 10 + set_local 8 + get_global 10 + i32.const 16 + i32.add + set_global 10 + get_global 10 + get_global 11 + i32.ge_s + if ;; label = @2 + i32.const 16 + call 0 + end + get_local 0 + set_local 6 + get_local 1 + set_local 5 + get_local 6 + set_local 2 + get_local 5 + set_local 3 + get_local 2 + get_local 3 + i32.add + set_local 4 + get_local 8 + set_global 10 + get_local 4 + return + unreachable + end + unreachable) + (func (;8;) (type 5) + (local i32) + nop) + (func (;9;) (type 5) + block ;; label = @1 + get_global 3 + i32.const 0 + i32.add + set_global 10 + get_global 10 + i32.const 5242880 + i32.add + set_global 11 + call 8 + end) + (global (;7;) (mut i32) (get_global 0)) + (global (;8;) (mut i32) (get_global 1)) + (global (;9;) (mut i32) (get_global 2)) + (global (;10;) (mut i32) (i32.const 0)) + (global (;11;) (mut i32) (i32.const 0)) + (global (;12;) (mut i32) (i32.const 0)) + (global (;13;) (mut i32) (i32.const 0)) + (global (;14;) (mut i32) (i32.const 0)) + (global (;15;) (mut i32) (i32.const 0)) + (global (;16;) (mut f64) (get_global 5)) + (global (;17;) (mut f64) (get_global 6)) + (global (;18;) (mut i32) (i32.const 0)) + (global (;19;) (mut i32) (i32.const 0)) + (global (;20;) (mut i32) (i32.const 0)) + (global (;21;) (mut i32) (i32.const 0)) + (global (;22;) (mut f64) (f64.const 0x0p+0 (;=0;))) + (global (;23;) (mut i32) (i32.const 0)) + (global (;24;) (mut f32) (f32.const 0x0p+0 (;=0;))) + (global (;25;) (mut f32) (f32.const 0x0p+0 (;=0;))) + (export "__post_instantiate" (func 9)) + (export "_addSpeed" (func 7)) + (export "runPostSets" (func 8)) +(export "_speed" (func 6))) +``` + +
+ +代码的分析直接看注释即可,没有太多的技术性细节。 + +
+Webassembly可以双向工作,既可以从JS调用WASM中的导出符号,也可以在WASM中调用导入的JS符号。在JS中,可以通过前面提到的API接口创建相关的表和内存等,并通过导入符号的方式将其导入到WASM中,这和多语言间的库的交换数据还是有些相似的。如果想对交互方式有更深入的了解,可以查看一下github: +
+ +https://github.com/mdn/webassembly-examples/tree/master/js-api-examples +
+
+ +## 三、总结 +这一块是整个Webassembly中最常用的部分,毕竟WASM的设计初衷就是为了解决在JS运行高效的代码。接口的设计好坏与否,直接关系到上层应用到底层调用的的方便易用性,特别是效率的影响,一定要引起重视。在IBM,mozilla以及github等官网上都有大师的相关文档供查阅。需要注意的一点就是,许多的细节随着版本的推进,已经变得过时。 diff --git "a/VM/evm-ewasm/wasm\347\232\204\347\274\226\350\257\221.md" "b/VM/evm-ewasm/wasm\347\232\204\347\274\226\350\257\221.md" new file mode 100644 index 0000000..621c3eb --- /dev/null +++ "b/VM/evm-ewasm/wasm\347\232\204\347\274\226\350\257\221.md" @@ -0,0 +1,90 @@ +# wasm的编译 + +# 一、WASM的编译器介绍 +Webassembly出现的时间并不长,从目前来看,主流的编译器有以下几类:Emscripten工具链,binaryen ,AssemblyScript。其实他们主要的还是要依赖于LLVM,不管前台用什么来编译,当到达IR这一层面时,目前来看,LLVM的处理仍然是主流。 +
+从目前提供的编译方式来看,有本地的编译环境,也有在线的编译环境。但它们背后使用的编译器基本上不会逃脱上面的几种编译工具。主流的几个在线编译器在前面提供了: +
+https://webassembly.studio/ +
+https://wasdk.github.io/WasmFiddle/ +
+本地工具链的安装可以采用官方文档: +
+https://emscripten.org/docs/getting_started/downloads.html +
+下面有一个整理好的方法: +
+ +```shell +$ git clone https://github.com/juj/emsdk.git +$ cd emsdk +$ ./emsdk install latest +$ ./emsdk activate latest +``` +在老的版本中,曾经提供过在Windows上的离线安装版本,但在新版本里,统一使用相关的安装方式了。 +# 二、相关编译技术 +编译器的作用简单说来就是一个,把人类看懂的语言翻译成机器可以执行的语言。至于在编译过程应用的各种技巧和方法,都是为了使编译更高效、安全、快捷。 +
+大家都知道现在代的编译器基本上分为两大类,即静态编译器和动态解释器。它们各有优势,目前在前端开发使命的语言中,以解释器为主,但随着技术的进步,二者之间的界限大有弥合的趋势。包括JAVA在内的虚拟机技术都提供了JIT(Just-In-Time)编译器。 +
+在传统的解释器中,最初的工作方式如下: +
+代码-记法分析-语法分析-语法树(AST)-AST遍历解释器-执行结果。 +这样的工作方式简单明了,但是却有一个重大的弊病,那就是效率太低下了,举一个简单的例子,有十个函数都调用了一个处理算法,那么这个算法会被解释十次,如果有更多的调用,会有更多的解释情况,这使得执行的效率成级数的下降。 +
+解决这个问题的方法就是提供一个虚拟机(VM)入在语法树后,如果有一些公共的热点代码经常被使用就编译成字节码在VM中执行。 +
+但是无论怎么样,效率的问题,仍然是解释器的一个致命的缺点,如果对效率要求不是很高的情况下,这个缺点倒也无所谓,但当引入一些执行效率很高的场景时,它显然就无法完成这项工作了,至少是不能很好的完成。 +而JIT就相对来说又进一步,它将虚拟机的执行分成了三部分,即解释执行,监视器监视,在解释器执行的过程中,如果发现一些代码被执行了多次,就会被标记为Warm,然后送往基线编译器(baseline compiler)进行编译,开成一个桩(stub),并给与其一个相关的标记索引(行号+变量类型)。这样,下次再执行这段代码时,就可以直接操作这个编译结果而不用去解释。 +
+基线编译器在应用时是有限制的,最主要的是其编译的时间不能过长,这就意味着基线编译器不能对代码进行优化。 +
+监视器在整个代码的解释过程中,如果发现某些代码形成了热点(即比Warm执行的次数还要多)那么这段代码会被送入优化编译器(optimizer compiler)进行优化。形成一个更高效的执行结果。 +
+但是这又引出了一个问题,JS等解释型语言,它的类型是动态确定的,如果在优化的过程中,发现优化的并不对,这就会引起所谓的“去优化”,即重新回到基线编译器甚至解释执行的过程。如果这种现象反复出现的话,就会导致执行效率反而更低。如何解决这种情况呢? +
+有两大阵营解决了这种缺陷,一个是将JS等语言静态化,形成一种类似于c++/Java等静态编译语言的机制,但这种方式有开历史倒车的嫌疑;另外一种是将语言的类型固定了有限的几种,比如asm.js只有32整形和64位浮点两种数据类型。 +
+但解决的方法越多,说明碎片化越严重,所以几大巨头推出了WASM,它干脆去掉了动态语言的解释过程,直接将其搞成了字节码形式(也就是说直接在机器架构的机器码上抽象了一层)。当然为了兼容,可以通过工具(polyfill)将其转换成前面的asm.js(JIT)等进行执行。 +但是,它更大的优势在于,它可以使用AOT(Ahead-Of-Time)直接转到机器码执行,那它的执行效率,应该和静态编译语言的执行效率,没有了量级上的差距了。 +
+在Wasm中,由于已经不需要对类型进行假设,所以上面的两大解决方案自然就被统一,所以目前来看,Wasm的编译优化上天然要比前面提到的技术要强。 + +# 三、WASM的编译过程 +一段代码是如何从源码被编译器翻译成字节码的呢?或者用一句不太准确的话来描述,源码是如何翻译成WASM类型的汇编格式的呢(WAST格式)? +
+它的一个基本流程如下: +
+源码(c++/C,rust,go等)——LLVM(IR)——字节码——机器码 +也就是说,源码通过各种前端(如Clang等)编译成LLVM IR,在这个过程中,LLVM自然会对相关的代码进行各种优化。在到达IR后,如果想将其转换为JS可执行的代码还需要LLVM的一个后端工具(Fastcomp)。 +从这里看,WASM的相关编译器很多仍然使用LLVM做为一种核心的编译工具,同时,LLVM正在开发一种专门的后端编译工具来处理这种情况。 +
+下面看一下代码的汇编文件: + +```c++ +wasm-function[0]: + sub rsp, 8 ; 0x000000 48 83 ec 08 + mov ecx, esi ; 0x000004 8b ce + mov eax, ecx ; 0x000006 8b c1 + add eax, edi ; 0x000008 03 c7 + nop ; 0x00000a 66 90 + add rsp, 8 ; 0x00000c 48 83 c4 08 + ret ; 0x000010 c3 + +wasm-function[1]: + sub rsp, 8 ; 0x000000 48 83 ec 08 + mov eax, 0x2a ; 0x000004 b8 2a 00 00 00 + nop ; 0x000009 66 90 + add rsp, 8 ; 0x00000b 48 83 c4 08 + ret ; 0x00000f c3 + +``` +这段汇编代码正是前面反复用到的那个简单的例子的汇编代码。这已经和普通的静态编译器编译出来的代码没有明显的区别了。 + +# 四、WASM的编译器发展 +从目前的情况来看,WASM仍然没有一款专属于自己的全链编译器,当然,现在也不敢肯定这种需求是必然的,不过针对WASM开发出相关的适配的编译器仍然是必然的。如果不是这样,包括LLVM也不会开展这方面的开发工作。WASM的编译器开发还有很多路要走,举一个明显的例子,现在都在做内存的自动管理,包括c++这种静态语言都推出了相关的智能指针,那么WASM这方面是不是也要有推进的脚步。这就涉及到了几乎所有虚拟机头疼的GC问题。这个不是说光靠编译器就能解决的。 + +# 五、总结 +在初步分析了WASM的编译器的应用技术后,可以看到一个很明显的特点,那就是WASM目前的编译技术还没有真正成熟起来,当然由于本身的标准制定都在进行中,所以这种想法也有一些苛求。 +编译工具链还有些复杂,虽然有了一些在线IDE支持相关的编译调试,但是目前来看,还有很大的改进的空间。 diff --git "a/VM/evm-ewasm/webassembly\346\212\200\346\234\257.md" "b/VM/evm-ewasm/webassembly\346\212\200\346\234\257.md" new file mode 100644 index 0000000..2de54be --- /dev/null +++ "b/VM/evm-ewasm/webassembly\346\212\200\346\234\257.md" @@ -0,0 +1,125 @@ +# webassembly技术 + +## 一、webassembly介绍 +wasm最近相当火,不光EOS默认的智能合约开发使用的是它,以太坊也已经决定在下一代的版本上采用ewasm做为虚拟机的实现(wasm的一个子集)。wasm为什么会被区块链如此重视?这得说它确实有相当出色的能力。 +
+在早期的软件开发中,基本是桌面开发,各种库用起来相当方便,便是到了后来,从CS转向BS后,发现了一个问题,如果想在Web前端实现复杂的应用场景,非常麻烦,随着各种脚本语言和相关的JS的框架的广泛流行,开发的便捷性和性能都有大幅提升,但是在一个非常重要的场景下,还是解决不了问题,那就是3D游戏。这时候就有相关方提出了asm.js、Dart 、typescript,它们各有优势,在不同的应用场景下都有很抢眼的表现。 +
+可是,技术的发展是要有统一的标准,而不是各自搞各自的,这种情况下,webassembly被提出了。好多人把它当成一种新的编程语言,但是目前来看,它更象是一种中间标准技术,甚至它只是一种文件格式。把它当成一门汇编语言,有点不是很准确。 +
+webassembly就是为了性能而生,它在Web前端的应用可以大幅的提高应用的流畅性。之所以有这些优越性,主要原因在于,它可以编译成非常稳定的wasm字节码格式,而llvm实现了llvm IR中间码到wasm的转换。也就是说,wasm其实并不是真正的汇编指令,而是对应的一些虚拟的指令,即他在IR到真正的机器架构间的一环。所以实现了执行WASM的环境可以叫做WASM虚拟机,比如EOS中的wasm-jit执行环境。 +
+在webassembly中有两种文件格式,即wat,wasm,另外还有一个wat的超集wast,可以这样理解,wast是人类易理解的文本格式,wasm是机器识别的格式。 +网上有很多的开发相关的工具和在线网站: +
+https://webassembly.studio/ +
+https://wasdk.github.io/WasmFiddle/ +
+https://webassembly.github.io/wabt/demo/wat2wasm/ +
+https://github.com/WebAssembly/wabt +
+https://github.com/emscripten-core/emscripten +
+https://github.com/AssemblyScript/assemblyscript + +## 二、现状 +webassembly主要是应用于前端浏览器,当然LINUX的内核也可以使用,但是主流是为了在前端提高性能。它的主要框架如下: + +![module](img/module.jpg) + +发展的路线图: +
+1、WebAssembly API和二进制格式的设计已完成。 +
+2、下一步准备统一解释器。 +
+3、将Emscripten稳定,实现LLVM和Webassembly的转换。 +
+4、将其应用到更多的浏览器并开发相关工具。 +
+5、启动 post-MVP(a Minimum Viable Product:最低可行产品) features,详细的内容参看官网 +
+Webassembly目前来看,既支持编译器编译执行,也支持解释器解释执行,当然,这就引出了类似热点代码的问题,可以解释,然后编译热点。目前主要的编译器基本以LLVM为主,所以看到资料基本都是IR到WASM,但是看其发展的目标,应该是直接到wasm,也就是说,由标准的虚拟指令取代中间代码(一个新的中间态)。所以最后发展成什么样子,还需要继续看。 + +# 三、应用 +1、浏览器中使用高效(C/C++等)代码 +
+举一个小例子: +
+ +```C++ +#define WASM_EXPORT __attribute__((visibility("default"))) +WASM_EXPORT +int add(int a, int b) { + return a + b; +} +WASM_EXPORT +int main() { + return 42; +} +``` +其对应的WAT文件: + +```C++ +(module + (type $t0 (func)) + (type $t1 (func (param i32 i32) (result i32))) + (type $t2 (func (result i32))) + (func $\__wasm_call_ctors (type $t0)) + (func $add (export "add") (type $t1) (param $p0 i32) (param $p1 i32) (result i32) + get_local $p1 + get_local $p0 + i32.add) + (func $main (export "main") (type $t2) (result i32) + i32.const 42) + (table $T0 1 1 anyfunc) + (memory $memory (export "memory") 2) + (global $g0 (mut i32) (i32.const 66560)) + (global $\__heap_base (export "\__heap_base") i32 (i32.const 66560)) + (global $\__data_end (export "\__data_end") i32 (i32.const 1024))) +``` +将这段代码拷贝到前面网址中的webassembly中,编译,就可以得到WASM文件并下载下来。同时它的右侧会有一个测试的JS提供调用的测试。如果你不想使用它的测试想自己来的话,就接着往下看: +
+写一个HTML文件中的JS脚本: +
+ +```html + +WASM + +``` +然后把这个用可以调试的浏览器(如Chrome等),打开,在控制台里,输入:window.add(1,2),则可以得到3.如果需要跨域,请使用各种跨域的方法来解决。 +
+ +2、多语言的中间化 +
+这个如果做好了,更有前途,比如c#语言,编译成WASM后,可以在LINUX平台上跑,完全解决了微软的各种套路。安卓的程序,编译好了,可以搞到苹果上开发,也不用什么各种跨平台的框架了。 +
+看它们的路线图里,也有这个野望。 + +## 四、不足 +虽然webassembly有很多优点,但作为一个新技术还是有很多不足之处: +
+1、新,就意味着应用不广泛,还有各种坑。同时,学习的资料不多并且社区都不是很成熟。 +
+2、对各种浏览器的支持不是很好,同时,不同的浏览器调用还是存在很多的兼容性问题。 +
+3、目前还没有一门专门的成熟的开发的语言。虽然有c++,Go,Rust等语言支持,但毕竟还不是原生支持的。 +
+ +# 五、总结 +从目前来看,webassembly仍然是一门正在发展的技术,虽然它的后面站着微软,谷歌等一票大公司,但距离成熟还有相当的距离。不过,webassembly的优势会让大家逐渐接受他并流行起来。如果有一门原生的语言直接支持webassembly后,相信会很快的打开一片市场。 diff --git "a/VM/evm-ewasm/\344\273\245\345\244\252\345\235\212\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" "b/VM/evm-ewasm/\344\273\245\345\244\252\345\235\212\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" new file mode 100644 index 0000000..a7d9dd1 --- /dev/null +++ "b/VM/evm-ewasm/\344\273\245\345\244\252\345\235\212\345\257\271WASM\347\232\204\346\224\257\346\214\201.md" @@ -0,0 +1,155 @@ +# 以太坊对WASM的支持 + +## 一、以太坊新版本中的WASM虚拟机 +以太坊的原生的EVM在使用中暴露了不少的问题,在下一个新的大的版本变更中,将提供一个基于WASM的ewasm文件格式。可以把认为是下一代虚拟机的技术标准。使用WASM的优势很多,但缺点也不少,最关键的是WASM技术本身正处在很快的发展变化过程中,它的自身并不是很完善。 +
+如果你想部署一个WASM的智能合约到测试网络testnet可以使用下面的网址: +
+http://ewasm.ethereum.org/studio/ + +## 二、EVMC +在EWASM的支持上,Parity对其支持的最好的(不愧是原来以太坊的技术老大),但是它将其内置到了客户端中,这和目前以太坊的设计发展是不一致的。现在以太坊的官方已经推出一个Hera的初步ewasm虚拟机,它支持以太坊提出的EVMC。EVMC是解除客户端和虚拟机的耦合的一个标准接口。 +
+ +![emvc-h](img/hera-struct.png) +
+在这种设计下,以太坊的客户端只负责和链上的信息交互,而合约的执行解析交给EVM。二者通过EVMC这个接口来通信。在EVMC中提供三部分即下图: +
+ +![emvc](img/emvc-struct.png) +
+ 这里HOST使用虚线的目的是表示这部分逻辑上是可以和EVMC隔离实现的。 + +## 三、对EVM的兼容 +以太坊的虚拟上已经部署了N多的基于Solidity的合约,如何与其相兼容,是一个很重要的问题。EWASM能过版本的匹配来判断是EVM1.0(Solidity)还是2.0(EWASM),如果老的版本,则使用evm2wasm将其翻译成wasm合约。在部署合约时,需要有一个预编译的内置合约对部署合约进行检查,即对EVM2.0的规范的匹配,比如不能出现float等。 +
+EWASM为了保证安全性,只提供对EEI模块的支持导入,不支持其它的Module。这样就可以把EWASM的运行控制在一个类似沙箱的环境内。 +
+EWASM只提供了开发时的Debug,在部署时,必须关闭。 + +## 四、EEI和ECI +EEI(Ethereum Environment Interface)和ECI(Ewasm Contract Interface),即以太坊的环境接口和Ewasm合约的接口。EEI是以太坊提供的一套上下文环境的相关接口标准,原则是只要实现了这个接口标准,就能够和以太坊的链进行交互并安全运行。其说明文档在: +
+https://github.com/ewasm/design/blob/master/eth_interface.md +
+而ECI是关于Ewasm的智能合约的数据结构和合约模块的接口规范(语义设计)。其说明文档地址在: +
+https://github.com/ewasm/design/blob/master/contract_interface.md +
+在已经实现的Hera中,分成了两部分,一部分在EVMC中,提供的标准的接口,另外一部分是在hera的eei.h中,提供了对EVMC的接口的封装和实现。可以看一下下面的代码: +
+ +```C++ +//emvc +typedef struct evmc_tx_context (*evmc_get_tx_context_fn)(struct evmc_context* context); +typedef evmc_bytes32 (*evmc_get_block_hash_fn)(struct evmc_context* context, int64_t number); +typedef void (*evmc_release_result_fn)(const struct evmc_result* result); +typedef bool (*evmc_account_exists_fn)(struct evmc_context* context, const evmc_address* address); +typedef evmc_bytes32 (*evmc_get_storage_fn)(struct evmc_context* context, + const evmc_address* address, + const evmc_bytes32* key); + +typedef enum evmc_storage_status (*evmc_set_storage_fn)(struct evmc_context* context, + const evmc_address* address, + const evmc_bytes32* key, + const evmc_bytes32* value); + +typedef evmc_uint256be (*evmc_get_balance_fn)(struct evmc_context* context, + const evmc_address* address); + +typedef size_t (*evmc_get_code_size_fn)(struct evmc_context* context, const evmc_address* address); + +typedef evmc_bytes32 (*evmc_get_code_hash_fn)(struct evmc_context* context, + const evmc_address* address); + +typedef size_t (*evmc_copy_code_fn)(struct evmc_context* context, + const evmc_address* address, + size_t code_offset, + uint8_t* buffer_data, + size_t buffer_size); + +typedef void (*evmc_selfdestruct_fn)(struct evmc_context* context, + const evmc_address* address, + const evmc_address* beneficiary); +typedef void (*evmc_emit_log_fn)(struct evmc_context* context, + const evmc_address* address, + const uint8_t* data, + size_t data_size, + const evmc_bytes32 topics[], + size_t topics_count); + +typedef struct evmc_result (*evmc_call_fn)(struct evmc_context* context, + const struct evmc_message* msg); + +typedef void (*evmc_destroy_fn)(struct evmc_instance* evm); + +//Hera中.eei.h eei.cpp +class EthereumInterface { +public: +...... + int64_t eeiGetBlockNumber(); + int64_t eeiGetBlockTimestamp(); + void eeiGetTxOrigin(uint32_t resultOffset); + void eeiStorageStore(uint32_t pathOffset, uint32_t valueOffset); + void eeiStorageLoad(uint32_t pathOffset, uint32_t resultOffset); + void eeiFinish(uint32_t offset, uint32_t size) { eeiRevertOrFinish(false, offset, size); } + void eeiRevert(uint32_t offset, uint32_t size) { eeiRevertOrFinish(true, offset, size); } + uint32_t eeiGetReturnDataSize(); + void eeiReturnDataCopy(uint32_t dataOffset, uint32_t offset, uint32_t size); + uint32_t eeiCall(EEICallKind kind, int64_t gas, uint32_t addressOffset, uint32_t valueOffset, uint32_t dataOffset, uint32_t dataLength); + uint32_t eeiCreate(uint32_t valueOffset, uint32_t dataOffset, uint32_t length, uint32_t resultOffset); +...... + void ensureSourceMemoryBounds(uint32_t offset, uint32_t length); + void loadMemoryReverse(uint32_t srcOffset, uint8_t *dst, size_t length); + void loadMemory(uint32_t srcOffset, uint8_t *dst, size_t length); + void loadMemory(uint32_t srcOffset, bytes& dst, size_t length); +...... +}; +//eei.cpp +void EthereumInterface::loadMemory(uint32_t srcOffset, bytes& dst, size_t length) +{ + // NOTE: the source bound check is not needed as the caller already ensures it + ensureCondition((srcOffset + length) >= srcOffset, InvalidMemoryAccess, "Out of bounds (source) memory copy."); + ensureCondition(memorySize() >= (srcOffset + length), InvalidMemoryAccess, "Out of bounds (source) memory copy."); + ensureCondition(dst.size() >= length, InvalidMemoryAccess, "Out of bounds (destination) memory copy."); + + if (!length) + HERA_DEBUG << "Zero-length memory load from offset 0x" << hex << srcOffset << dec <<"\n"; + + for (uint32_t i = 0; i < length; ++i) { + dst[i] = memoryGet(srcOffset + i); + } +} + +void EthereumInterface::storeMemoryReverse(const uint8_t *src, uint32_t dstOffset, uint32_t length) +{ + ensureCondition((dstOffset + length) >= dstOffset, InvalidMemoryAccess, "Out of bounds (destination) memory copy."); + ensureCondition(memorySize() >= (dstOffset + length), InvalidMemoryAccess, "Out of bounds (destination) memory copy."); + + if (!length) + HERA_DEBUG << "Zero-length memory store to offset 0x" << hex << dstOffset << dec << "\n"; + + for (uint32_t i = 0; i < length; ++i) { + memorySet(dstOffset + length - (i + 1), src[i]); + } +} + +void EthereumInterface::storeMemory(const uint8_t *src, uint32_t dstOffset, uint32_t length) +{ + ensureCondition((dstOffset + length) >= dstOffset, InvalidMemoryAccess, "Out of bounds (destination) memory copy."); + ensureCondition(memorySize() >= (dstOffset + length), InvalidMemoryAccess, "Out of bounds (destination) memory copy."); + + if (!length) + HERA_DEBUG << "Zero-length memory store to offset 0x" << hex << dstOffset << dec << "\n"; + + for (uint32_t i = 0; i < length; ++i) { + memorySet(dstOffset + i, src[i]); + } +} +``` + +
+代码本身没有什么可讲的,基本就是相关以太坊的操作。整个对WASM支持的基础是在ECI和EEI中,接口在EVMC中,实现的细节在EEI和相关的虚拟机,目前对二进制的虚拟机执行支持的已经基本完备,但WABT和WAVM尚在完善过程中。 + +## 五、总结 +通过从整体架构分析,基本可以清楚,EWASM2.0是以EVMC为接口支持,限制实现了WASM的功能。目前以太坊的客户端有很多。活跃就有好几个,有GO,c++和RUST等几个版本。对EWASM的支持又不尽相同。而通过EVMC隔离后,虚拟机的实现也有几个版本,比如e3vm。所以要从根本上把握设计的整体框架,再来看具体的实现,才能够更好的理解开发者的意图,并在此基础上,实现开源的贡献。 diff --git a/VM/pub/img/first_wabt.png b/VM/pub/img/first_wabt.png new file mode 100644 index 0000000..8d344ef Binary files /dev/null and b/VM/pub/img/first_wabt.png differ diff --git "a/VM/pub/wabt\347\232\204\344\270\200\344\270\252\351\232\220\346\200\247\345\206\205\345\255\230\351\227\256\351\242\230.md" "b/VM/pub/wabt\347\232\204\344\270\200\344\270\252\351\232\220\346\200\247\345\206\205\345\255\230\351\227\256\351\242\230.md" new file mode 100644 index 0000000..703b7c4 --- /dev/null +++ "b/VM/pub/wabt\347\232\204\344\270\200\344\270\252\351\232\220\346\200\247\345\206\205\345\255\230\351\227\256\351\242\230.md" @@ -0,0 +1,88 @@ +# wabt的一个隐性内存问题 + +# 一、WABT项目 +WABT项目是Webassembly的一个基础的软件集,在许多项目中都被引用到,诸如以太坊的下一代虚拟机Hera,EOS的虚拟机等。它提供了一整套的Webassembly的相关工具,支持Wasm的各种文件之间便捷快速的相互转换。其项目地址在: +
+https://github.com/WebAssembly/wabt + +# 二、WABT的问题 +这次发现的内存问题在wasm2c这个项目中,位于文件wasm-rt-impl.c中,函数wasm_rt_register_func_type中使用了realloc这个内存分配函数,看一下代码: +
+ +```C++ +uint32_t wasm_rt_register_func_type(uint32_t param_count, + uint32_t result_count, + ...) { + FuncType func_type; +...... + + uint32_t idx = g_func_type_count++; + //主要是下面这个重新分配内存的动作 + g_func_types = realloc(g_func_types, g_func_type_count * sizeof(FuncType)); + g_func_types[idx] = func_type; + return idx + 1; +} +``` +这里涉及到了realloc的经典应用问题两个中的一个,这两个经典的应用问题如下: +
+ +## 1、分配失败的检测问题 +先看代码: +
+ +```C++ +void *p = realloc(p,size); +if (!p) +{ + //处理并返回错误 + ...... + return err; +} +``` +
+这里本身就有一个问题,即当realloc调用失败时,此时返回一个NULL指针,那么,p就会被污染,导致原有的内存区泄露。另外,如果此处分配失败,在后面使用分配的内存没有判断时,一个Crash应该是不可避免的。那么正确的用法应该是什么样子呢? +
+ +```C++ +void *pTmp = realloc(p,size); +if (!pTmp) +{ + //处理并返回错误 + ...... + return err; +} + +p = pTmp; +``` +
+可能代码看上去不如上面的优雅,但是却安全了。 + +## 2、0内存分配使用的问题 +在C语言中,使用malloc(0)是正确的,而且可以用free却释放返回的指针,不会有任何问题。但是看一下realloc这个函数使用0做分配参数时,会是什么结果呢? +
+“Otherwise, if size is zero, the memory previously allocated at ptr is deallocated as if a call to free was made, and a null pointer is returned.(c/c++98) +
+If size is zero, the return value depends on the particular library implementation: it may either be a null pointer or some other location that shall not be dereferenced.(c/c++11)” +
+其实就是在早期版本0意味着和free相同,后来的新版本,就得看具体环境的实现了。其实后者更可怕,未知的东西有可能就是在东家好,在西家就用不了。还不如直接报错呢。看一下代码: +
+ +```C++ +void *pTmp = realloc(p,0); +free(pTmp) +``` +这等于是重复释放内存,直接就会崩溃。 +
+从上面可以看到,这里的问题,其实就是第一个分配失败的检测问题,但是第二个希望也引起重视。 + +# 三、修复 +这个BUG目前已经提交到了GITHUB,并生成了一个新的修改分支“wasm2c-realloc”,截图如下: +
+ +![realloc](img/first_wabt.png) + +
+修复可能会持续一小段时间。 + +# 四、总结 +通过上述分析可以看出,在c++中对内存的控制管理比在纯c中要复杂的多,但正因为C简单,才会让好多人大意,如果把相关的内存分配函数分析明白,其实这个问题是不应该出现的。 diff --git a/VM/solc/img/assembly.png b/VM/solc/img/assembly.png new file mode 100644 index 0000000..60e770c Binary files /dev/null and b/VM/solc/img/assembly.png differ diff --git a/VM/solc/img/c-p.png b/VM/solc/img/c-p.png new file mode 100644 index 0000000..eff8855 Binary files /dev/null and b/VM/solc/img/c-p.png differ diff --git a/VM/solc/img/compiler-c.png b/VM/solc/img/compiler-c.png new file mode 100644 index 0000000..ee1d535 Binary files /dev/null and b/VM/solc/img/compiler-c.png differ diff --git a/VM/solc/img/compiler.png b/VM/solc/img/compiler.png new file mode 100644 index 0000000..135e902 Binary files /dev/null and b/VM/solc/img/compiler.png differ diff --git a/VM/solc/img/parse.png b/VM/solc/img/parse.png new file mode 100644 index 0000000..d7f9eae Binary files /dev/null and b/VM/solc/img/parse.png differ diff --git "a/VM/solc/solc\347\274\226\350\257\221\345\231\250\345\210\235\346\255\245\345\210\206\346\236\220.md" "b/VM/solc/solc\347\274\226\350\257\221\345\231\250\345\210\235\346\255\245\345\210\206\346\236\220.md" new file mode 100644 index 0000000..a505cad --- /dev/null +++ "b/VM/solc/solc\347\274\226\350\257\221\345\231\250\345\210\235\346\255\245\345\210\206\346\236\220.md" @@ -0,0 +1,166 @@ +# solc编译器分析 +
+ +## 一、solc介绍 +
+以太坊的智能合约经过编译后运行在虚拟机上,完成整个工作流程,这个过程,需要solc编译器来完成。虽然说solc是一个编译器,但是它又有它与主流编译器不同之处。主要表现在如下几点: +
+ +### 1、功能简单 +
+支持的基本数据类型只有有限的几种,不支持浮点(或者说想支持,但是目前还没实现)。编译指令也较其它编译器少很多,功能单一。在完成编译时需要借助一些其它辅助的工具。 +
+换一句话说,以太坊的具体需求,也不需要它提供类似于c++等编译器的强大的编译能力。 +
+ +### 2、安全性弱 +
+虽然solc也支持各种处理数据私有的方法,但是由于区块链的先天性问题,部署到链上后,可以通过各种手段来访问和查看私有数据。而且目前以太坊尚未对数据提供混淆和加密的机制。 +
+ +### 3、对主流的异常处理机制支持较弱 +
+当前主流的编译器基本对异常各种操控都已经非常全面,各种理论和技术也不断涌现,但是以太坊的编译器对异常的处理机制支持的比较简单,流于表面化。 +
+ +### 4、不考虑平台相关 +
+这也是一个非常重要的事项,一个主流的编译器应该是支持市场上主流的操作系统,但是在以太坊上没有这个要求,因为它只服务于以太坊,甚至说对于升级版本的考虑都很简单。 +
+ +## 二、编译器流程 +
+目前主流的编译系统基本分为两大类,即编译类型和解释类型。二者各有各的优势,所以在近年来,二者融合的趋势只增不减。但是编译的原理基本没有发生革命性的变化。 +
+目前JAVA和C#的编译器都采用了热点编译,普通解释的方式。(这样表述其实并不太合理,JAVA其实是编译成字节码后,如果编译器发现某些代码被经常执行,就会再次汇编成类似C/C++一样的目标代码,更具体的可以参看JAVA虚拟机相关资料) +
+下面简要分析一下: +
+ +### 1、编译方式 +
+ +最典型的就是C/C++的编译器了,它们基本分为以下几个步骤: +
+ +预编译:在这个阶段主要是使用“预处理程序”来处理源代码,比如C/C++中会把宏和头文件处理并包含到编译单元中。 +
+ +词法分析:将源代码分割成不可再分割的单词,主要针对token进行处理归类等。 +
+ +语法分析:将提取的单词连接成单词序列,并根据编程语言规则验证其顺序是否合理。在这里会形成大家熟知的抽象语法树(Abstract Syntax Tree) +
+ +语义分析:发现符合语法规则的语句是否有实际意义。即遍历整个语法树进行判断。 +
+ +抽象语法树:这里即根据前边的各种条件处理后,最终生成的AST。 +
+ +中间码生成:不同的目标平台有着不同的运行机制,所以在这里要根据平台兼容性动态生成中间代码文件。 +
+ +目标代码生成:最终的汇编文件。 +
+目标代码优化:在生成汇编文件前的优化。 +
+ +其实后二者是在一起进行的。在一些强类型的编译器中,比如编译c/c++中,可能跳过中间码的生成而由汇编阶段再进行目标代码的生成和优化。也就是说,不同的编译器针对不同的环境或者场景,会有自己的优化和侧重点,这个不要一概而论。下图为其基本流程: +
+ +![编译图](img/compiler.png) +
+### 2、解释方式 +
+需要说明一下的是,不要被名字误导,解释器也是要进行编译的,只是它编译的过程没有编译方式那么复杂,特别是针对在类似JAVA这种虚拟机上执行的代码,还和虚拟机的相关规范有关系。它主要有以下几个步骤: +
+ +预处理 +
+ +词法分析 +
+ +语法分析 +
+ +生成语法树 +
+ +生成字节码 +
+通过上面来看,基本和编译方式的差不太多,对,往后面就开始有区别了。首先,JAVA和C#走到这里,它的整个工作流程基本就走完成了。当然二者的虚拟机都会针对具体的情况在运行时再次进行优化和编译。但那个就属于优化的范畴了。 +
+但是JS不同,JavaScript还要进行LLInt(Low Level Interpreter 解释器)执行字节码,最后会用JIT即时编译器再进行具体的编译和优化。所以说,不是一谈解释器就把它和编译器搞成两个山头,不是你死就是我活。下图为解释器工作的基本流程: +
+ +![解释器](img/parse.png) +
+ +### 3、WebAssembly +
+其实提到WebAssembly,就是因为区块链现在好多都在向这个方向上转,包括以太坊和目前大火的EOS。它的主要流程如下图: +
+ +![WebAssembly](img/c-p.png) +
+WebAssembly把编译分成了前端和后端,它把中间代码做得非常强大,可以使用相关的工具将IR中间代码和相关的平台代码相互转换,同时还可以实现IR的不同表现形式————JSON或者二进制等,方便人和机器识别,从而使应用更加灵活方便。 +
+ +## 三、solc的代码模块 +
+solc的代码主要由libdevcore 、libevmasm、 libjulia 、libsolc、 libsolidity 几个模块组成。在主目录下的CMakeLists.txt中可以看到: +
+ +``` c++ +add_subdirectory(libdevcore) +add_subdirectory(libevmasm) +add_subdirectory(libsolidity) +add_subdirectory(libsolc) +if (NOT EMSCRIPTEN) + add_subdirectory(solc) + add_subdirectory(liblll) + add_subdirectory(lllc) +endif() +``` +
+libdevcore主要提供了基础的数据操作和编解码操作,libjulia是用来处理YUL相关,libsolc和 libsolidity提供了大量的词法语法分析的类,比如声明定义、语法检查、类型检查等等。并且还提供了ABI生成和bin生成相关的操作。 + +
+ + +## 四、SOLC的简要分析 +
+在分析solidity的源码时要注意LLL已经基本废弃了,所以这部分代码就不再关注了。在solc的主程序中启动调用了CommandLineInterface这个接口类,在这个接口类中首先调用parseArguments来确定各种编译的环境参数。如果出现什么问题,就直接退出。这点有些类似于LINUX的命令机制。而在主程序的最后,调用actOnInput这个函数来实现编译结果的文件序列化。 +
+需要注意的是,未来版本的 Solidity 编译器很可能会将 Yul 用作智能合约编译的中间语言(IR),因此在solc源码中会出现很多与其相关的代码。 +
+其实solc编译的重点就在processInput这个函数中,主要就是下面三块: +
+ +### 1、complier +
+调用扫描和编译单元等模块生成AST,并在这个过程中完成各种验证和词法语法的分析。编译过程的主要类图如下: +
+ +![complier](img/compiler-c.png) +
+ +### 2、assembly +
+汇编成相关的栈文件,包括YUL,EWASM等。这其中会AsmAnalyzer、Assembly等类进行流程的处理和异常的控制。汇编过程的主要类图如下: +
+ +![assembly](img/assembly.png) +
+ +### 3、link +
+处理相关的链接库和文件。如果在编译过程中需要相关库和文件,就会调用链接函数对智能合约需要的资源进行链接。其实就是字节码的映射过程。 +
+ +## 五、总结 +
+solc做为智能合约的编译器,目前的资料还比较少,抛砖引玉,希望把solc的相关内容引入到EVM的分析中,给大家提供一个新的思路。 +
diff --git "a/VM/solc/\346\231\272\350\203\275\345\220\210\347\272\246\347\232\204bytecode\345\210\206\346\236\220.md" "b/VM/solc/\346\231\272\350\203\275\345\220\210\347\272\246\347\232\204bytecode\345\210\206\346\236\220.md" new file mode 100644 index 0000000..9b38b74 --- /dev/null +++ "b/VM/solc/\346\231\272\350\203\275\345\220\210\347\272\246\347\232\204bytecode\345\210\206\346\236\220.md" @@ -0,0 +1,340 @@ +# 以太坊智能合约的bytecode分析 + +## 一、测试合约 +以太坊的智能合约Solidity的指令并不多,所以这里不展开合约的指令的介绍,资料有很多。给出一个测试的实例: + +``` +pragma solidity ^0.4.25; +contract Example { + function OfTest(uint8 x,uint8 y) public returns (uint8){ + return x + y; + } +} +``` +合约非常简单,目的就是为了更清晰的看到跳转的情况,就是一个简单的8位无符号的整型相加。然后一个相同的类型(有溢出的风险)。 + +## 二、合约的类汇编代码 +上面的合约放到Remix后进行编译,得到如下的类汇编代码: + +``` +.code + PUSH 80 contract Example {\n\n func... + PUSH 40 contract Example {\n\n func... + MSTORE contract Example {\n\n func... + CALLVALUE contract Example {\n\n func... + DUP1 olidity ^ + ISZERO a + PUSH [tag] 1 a + JUMPI a + PUSH 0 a + DUP1 n + REVERT .25;\ncontrac +tag 1 a + JUMPDEST a + POP contract Example {\n\n func... + PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000 contract Example {\n\n func... + DUP1 contract Example {\n\n func... + PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 contract Example {\n\n func... + PUSH 0 contract Example {\n\n func... + CODECOPY contract Example {\n\n func... + PUSH 0 contract Example {\n\n func... + RETURN contract Example {\n\n func... +.data + 0: + .code + PUSH 80 contract Example {\n\n func... + PUSH 40 contract Example {\n\n func... + MSTORE contract Example {\n\n func... + PUSH 4 contract Example {\n\n func... + CALLDATASIZE contract Example {\n\n func... + LT contract Example {\n\n func... + PUSH [tag] 1 contract Example {\n\n func... + JUMPI contract Example {\n\n func... + PUSH 0 contract Example {\n\n func... + CALLDATALOAD contract Example {\n\n func... + PUSH 100000000000000000000000000000000000000000000000000000000 contract Example {\n\n func... + SWAP1 contract Example {\n\n func... + DIV contract Example {\n\n func... + PUSH FFFFFFFF contract Example {\n\n func... + AND contract Example {\n\n func... + DUP1 contract Example {\n\n func... + PUSH CDEAD58 contract Example {\n\n func... + EQ contract Example {\n\n func... + PUSH [tag] 2 contract Example {\n\n func... + JUMPI contract Example {\n\n func... + tag 1 contract Example {\n\n func... + JUMPDEST contract Example {\n\n func... + PUSH 0 contract Example {\n\n func... + DUP1 contract Example {\n\n func... + REVERT contract Example {\n\n func... + tag 2 function OfTest(uint8 x,uint8 ... + JUMPDEST function OfTest(uint8 x,uint8 ... + CALLVALUE function OfTest(uint8 x,uint8 ... + DUP1 olidity ^ + ISZERO a + PUSH [tag] 3 a + JUMPI a + PUSH 0 a + DUP1 n + REVERT .25;\ncontrac + tag 3 a + JUMPDEST a + POP function OfTest(uint8 x,uint8 ... + PUSH [tag] 4 function OfTest(uint8 x,uint8 ... + PUSH 4 function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + CALLDATASIZE function OfTest(uint8 x,uint8 ... + SUB function OfTest(uint8 x,uint8 ... + DUP2 function OfTest(uint8 x,uint8 ... + ADD function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + CALLDATALOAD function OfTest(uint8 x,uint8 ... + PUSH FF function OfTest(uint8 x,uint8 ... + AND function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + PUSH 20 function OfTest(uint8 x,uint8 ... + ADD function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + SWAP3 function OfTest(uint8 x,uint8 ... + SWAP2 function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + CALLDATALOAD function OfTest(uint8 x,uint8 ... + PUSH FF function OfTest(uint8 x,uint8 ... + AND function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + PUSH 20 function OfTest(uint8 x,uint8 ... + ADD function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + SWAP3 function OfTest(uint8 x,uint8 ... + SWAP2 function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + PUSH [tag] 5 function OfTest(uint8 x,uint8 ... + JUMP function OfTest(uint8 x,uint8 ... + tag 4 function OfTest(uint8 x,uint8 ... + JUMPDEST function OfTest(uint8 x,uint8 ... + PUSH 40 function OfTest(uint8 x,uint8 ... + MLOAD function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + DUP3 function OfTest(uint8 x,uint8 ... + PUSH FF function OfTest(uint8 x,uint8 ... + AND function OfTest(uint8 x,uint8 ... + PUSH FF function OfTest(uint8 x,uint8 ... + AND function OfTest(uint8 x,uint8 ... + DUP2 function OfTest(uint8 x,uint8 ... + MSTORE function OfTest(uint8 x,uint8 ... + PUSH 20 function OfTest(uint8 x,uint8 ... + ADD function OfTest(uint8 x,uint8 ... + SWAP2 function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + PUSH 40 function OfTest(uint8 x,uint8 ... + MLOAD function OfTest(uint8 x,uint8 ... + DUP1 function OfTest(uint8 x,uint8 ... + SWAP2 function OfTest(uint8 x,uint8 ... + SUB function OfTest(uint8 x,uint8 ... + SWAP1 function OfTest(uint8 x,uint8 ... + RETURN function OfTest(uint8 x,uint8 ... + tag 5 function OfTest(uint8 x,uint8 ... + JUMPDEST function OfTest(uint8 x,uint8 ... + PUSH 0 uint8 + DUP2 y + DUP4 x + ADD x + y + SWAP1 return x + y + POP return x + y + SWAP3 function OfTest(uint8 x,uint8 ... + SWAP2 function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + POP function OfTest(uint8 x,uint8 ... + JUMP [out] function OfTest(uint8 x,uint8 ... + .data +``` +在汇编的代码里可以清楚的看到来回跳转的过程,通过tag来标示一段代码的过程,这和c和c++中的汇编代码保持了一致。 +智能的汇编代码的开始的80和40是指空闲的内存地址和内存地址的指针的位置。这些汇编代码和下面的opcodes以及执行器执行的代码过程是保持一致的。 + +## 三、ABI接口和参数 +其ABI和相关的函数接口如下: + +``` +ABI: +[ + { + "constant": false, + "inputs": [ + { + "name": "x", + "type": "uint8" + }, + { + "name": "y", + "type": "uint8" + } + ], + "name": "OfTest", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] +FUNCTION: +{ + "0cdead58": "OfTest(uint8,uint8)" +} +``` +在Go版本的以太坊的源码中,提供了ABI的处理包,可以通过Pack等相关的函数来动态取得整个函数调用的二进制数据。不过其有一个前提,必须得提供ABI的字符串,也就是上面的这串字符串。需要说明的是,在GO版本的ABI字符串处理中,不能使用普通的JOSN处理库,无法解析,只能使用本身提供的相关的解析代码,有兴趣可以看一下源码中的abi包中的相关部分。 + +## 四、执行器及流程 +通过编译可以得到相关的二进制的字节码RUNTIMECODE(注意和BYTECODE的不同): +``` +"608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630cdead58146044575b600080fd5b348015604f57600080fd5b50607c +600480360381019080803560ff169060200190929190803560ff1690602001909291905050506098565b604051808260ff1660ff16815260200191505060405180910390f35b60008183019050929150505600a +165627a7a723058203dac5a701e6b6285a27cce8bfe10b3c06af990d564e269625b825172db61e9b40029", + "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT ..." +``` +将上面的字节部分拷贝出来就可以把这个合约部署到链上了。下面的“opcodes”是对应的翻译好的编译指令,而在执行器中就是按照这个顺序来进行执行的,下面分析一下执行的流程(注意和上面的汇编代码的指令流程相匹配): +``` +000 PUSH1 80 ;压入栈 +002 PUSH1 40 +004 MSTORE ;开辟内存空间 +005 PUSH1 04 +007 CALLDATASIZE +008 LT ;比较4和CALLDATASIZE是否后者小 +009 PUSH1 3f ;压入回滚地址 +011 JUMPI ;上面的比较如果小,则跳到回滚地址 +012 PUSH1 00 +014 CALLDATALOAD ;从00处开始取得数据 +015 PUSH29 0100000000000000000000000000000000000000000000000000000000 +045 SWAP1 +046 DIV ;DIV和上面的PUSH29表示左移28字节,一共32字节(256位),余下四字节为函数哈希 +047 PUSH4 ffffffff +052 AND ; 保留低四字节 +053 DUP1 +054 PUSH4 0cdead58 ;OfTest函数的keccak256 +059 EQ ;比较是否相等 +060 PUSH1 44 +062 JUMPI ;相等则到到44处,注意十六进制:0x44 +063 JUMPDEST ;空指令 +064 PUSH1 00 +066 DUP1 +067 REVERT ;回滚,说明函数匹配没有成功 +068 JUMPDEST ;匹配成功进行下列操作 +069 CALLVALUE ;取出交易额 +070 DUP1 +071 ISZERO ;是否为0 +072 PUSH1 4f +074 JUMPI ;不为0跳到4f +075 PUSH1 00 +077 DUP1 +078 REVERT ;回滚并退出 +079 JUMPDEST +080 POP +081 PUSH1 7c +083 PUSH1 04 +085 DUP1 ;复制栈顶 +086 CALLDATASIZE +087 SUB +088 DUP2 +089 ADD +090 SWAP1 +091 DUP1 +092 DUP1 +093 CALLDATALOAD ;加载数据 +094 PUSH1 ff +096 AND +097 SWAP1 +098 PUSH1 20 +100 ADD +101 SWAP1 +102 SWAP3 +103 SWAP2 +104 SWAP1 +105 DUP1 +106 CALLDATALOAD +107 PUSH1 ff +109 AND +110 SWAP1 +111 PUSH1 20 +113 ADD +114 SWAP1 +115 SWAP3 +116 SWAP2 +117 SWAP1 +118 POP +119 POP +120 POP +121 PUSH1 98 +123 JUMP ;跳到0x98,执行函数 +124 JUMPDEST ;函数调用完成后用来处理堆栈 +125 PUSH1 40 +127 MLOAD +128 DUP1 +129 DUP3 +130 PUSH1 ff +132 AND ;uint8,只取八位(ff),溢出的风险就在这。它使用AND截取 +133 PUSH1 ff +135 AND +136 DUP2 +137 MSTORE +138 PUSH1 20 +140 ADD +141 SWAP2 +142 POP +143 POP +144 PUSH1 40 +146 MLOAD +147 DUP1 +148 SWAP2 +149 SUB +150 SWAP1 +151 RETURN ;返回 +152 JUMPDEST +153 PUSH1 00 ;此处开始函数内部执行即x+y +155 DUP2 +156 DUP4 +157 ADD +158 SWAP1 +159 POP +160 SWAP3 +161 SWAP2 +162 POP +163 POP +164 JUMP ;此处跳转到124处开始处理返回堆栈 +165 STOP +-----------------END----------------------- +166 LOG1 +167 PUSH6 627a7a723058 +174 SHA3 +175 PUSH7 629659995f4ebe +183 INVALID +184 PUSH10 b64b24559e73396aa826 +195 INVALID +196 INVALID +197 CALLDATASIZE +198 SWAP8 +199 INVALID +200 PUSH22 61c781c1e8fa0029 +``` +上面的代码注释很多,就不再详述,只说一下CALLDATALOAD这个指令,看一下源码中如何实现的: + +```golang +func opCallDataLoad(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { + stack.push(interpreter.intPool.get().SetBytes(getDataBig(contract.Input, stack.pop(), big32))) + return nil, nil +} +``` +在虚拟机执行调用函数前,会把对应的参数存储到contract.Input,而此时则把它加载到堆栈中。把执行过程的代码和上面的汇编代码对比,可以发现调用的过程和汇编实现的过程完全相同的,只是在对比时,注意代码的原始过程和执行流程是不同的(执行过程中会有JUMP)。但是,整体的静态状态,是没有区别的。 + +## 五、总结 +以太坊的Solidity会不会退出历史舞台,得看EWASM的开发进度和人们的接受程度。只要弄明白了Solidity的虚拟机流程,那么在EWASM中也不会有革命的变化,只不过是换了一种比较安全和普及的手段罢了,设计的思想是没有太大的变化的。 diff --git a/baidu-salon/img/IMG_20191121_135831.jpg b/baidu-salon/img/IMG_20191121_135831.jpg new file mode 100644 index 0000000..04b789d Binary files /dev/null and b/baidu-salon/img/IMG_20191121_135831.jpg differ diff --git a/baidu-salon/img/IMG_20191121_140753.jpg b/baidu-salon/img/IMG_20191121_140753.jpg new file mode 100644 index 0000000..f571f0f Binary files /dev/null and b/baidu-salon/img/IMG_20191121_140753.jpg differ diff --git a/baidu-salon/img/IMG_20191121_140948.jpg b/baidu-salon/img/IMG_20191121_140948.jpg new file mode 100644 index 0000000..e95577a Binary files /dev/null and b/baidu-salon/img/IMG_20191121_140948.jpg differ diff --git a/baidu-salon/img/IMG_20191121_140950.jpg b/baidu-salon/img/IMG_20191121_140950.jpg new file mode 100644 index 0000000..88593b9 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_140950.jpg differ diff --git a/baidu-salon/img/IMG_20191121_143408.jpg b/baidu-salon/img/IMG_20191121_143408.jpg new file mode 100644 index 0000000..e1e3cec Binary files /dev/null and b/baidu-salon/img/IMG_20191121_143408.jpg differ diff --git a/baidu-salon/img/IMG_20191121_144932.jpg b/baidu-salon/img/IMG_20191121_144932.jpg new file mode 100644 index 0000000..e949e25 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_144932.jpg differ diff --git a/baidu-salon/img/IMG_20191121_144937.jpg b/baidu-salon/img/IMG_20191121_144937.jpg new file mode 100644 index 0000000..dcba237 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_144937.jpg differ diff --git a/baidu-salon/img/IMG_20191121_145141.jpg b/baidu-salon/img/IMG_20191121_145141.jpg new file mode 100644 index 0000000..7f2835b Binary files /dev/null and b/baidu-salon/img/IMG_20191121_145141.jpg differ diff --git a/baidu-salon/img/IMG_20191121_152342.jpg b/baidu-salon/img/IMG_20191121_152342.jpg new file mode 100644 index 0000000..c83a132 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_152342.jpg differ diff --git a/baidu-salon/img/IMG_20191121_153336.jpg b/baidu-salon/img/IMG_20191121_153336.jpg new file mode 100644 index 0000000..ba5f7dd Binary files /dev/null and b/baidu-salon/img/IMG_20191121_153336.jpg differ diff --git a/baidu-salon/img/IMG_20191121_153407.jpg b/baidu-salon/img/IMG_20191121_153407.jpg new file mode 100644 index 0000000..23f59d8 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_153407.jpg differ diff --git a/baidu-salon/img/IMG_20191121_154730.jpg b/baidu-salon/img/IMG_20191121_154730.jpg new file mode 100644 index 0000000..f1ca752 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_154730.jpg differ diff --git a/baidu-salon/img/IMG_20191121_154733.jpg b/baidu-salon/img/IMG_20191121_154733.jpg new file mode 100644 index 0000000..5407dd1 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_154733.jpg differ diff --git a/baidu-salon/img/IMG_20191121_154746.jpg b/baidu-salon/img/IMG_20191121_154746.jpg new file mode 100644 index 0000000..ab9b0c5 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_154746.jpg differ diff --git a/baidu-salon/img/IMG_20191121_154748.jpg b/baidu-salon/img/IMG_20191121_154748.jpg new file mode 100644 index 0000000..003875f Binary files /dev/null and b/baidu-salon/img/IMG_20191121_154748.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155310.jpg b/baidu-salon/img/IMG_20191121_155310.jpg new file mode 100644 index 0000000..8c5489f Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155310.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155315.jpg b/baidu-salon/img/IMG_20191121_155315.jpg new file mode 100644 index 0000000..aba5772 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155315.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155330.jpg b/baidu-salon/img/IMG_20191121_155330.jpg new file mode 100644 index 0000000..9bac8c4 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155330.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155421.jpg b/baidu-salon/img/IMG_20191121_155421.jpg new file mode 100644 index 0000000..c9366d0 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155421.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155422.jpg b/baidu-salon/img/IMG_20191121_155422.jpg new file mode 100644 index 0000000..f23585b Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155422.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155424.jpg b/baidu-salon/img/IMG_20191121_155424.jpg new file mode 100644 index 0000000..10b77e6 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155424.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155443.jpg b/baidu-salon/img/IMG_20191121_155443.jpg new file mode 100644 index 0000000..6c8bd12 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155443.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155549.jpg b/baidu-salon/img/IMG_20191121_155549.jpg new file mode 100644 index 0000000..c1d4f67 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155549.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155554.jpg b/baidu-salon/img/IMG_20191121_155554.jpg new file mode 100644 index 0000000..4e00035 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155554.jpg differ diff --git a/baidu-salon/img/IMG_20191121_155555.jpg b/baidu-salon/img/IMG_20191121_155555.jpg new file mode 100644 index 0000000..3204772 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_155555.jpg differ diff --git a/baidu-salon/img/IMG_20191121_162149.jpg b/baidu-salon/img/IMG_20191121_162149.jpg new file mode 100644 index 0000000..510ea42 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_162149.jpg differ diff --git a/baidu-salon/img/IMG_20191121_162155.jpg b/baidu-salon/img/IMG_20191121_162155.jpg new file mode 100644 index 0000000..992d53d Binary files /dev/null and b/baidu-salon/img/IMG_20191121_162155.jpg differ diff --git a/baidu-salon/img/IMG_20191121_163357.jpg b/baidu-salon/img/IMG_20191121_163357.jpg new file mode 100644 index 0000000..af85199 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_163357.jpg differ diff --git a/baidu-salon/img/IMG_20191121_163806.jpg b/baidu-salon/img/IMG_20191121_163806.jpg new file mode 100644 index 0000000..68c259f Binary files /dev/null and b/baidu-salon/img/IMG_20191121_163806.jpg differ diff --git a/baidu-salon/img/IMG_20191121_164142.jpg b/baidu-salon/img/IMG_20191121_164142.jpg new file mode 100644 index 0000000..0114912 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_164142.jpg differ diff --git a/baidu-salon/img/IMG_20191121_164146.jpg b/baidu-salon/img/IMG_20191121_164146.jpg new file mode 100644 index 0000000..79fc9fd Binary files /dev/null and b/baidu-salon/img/IMG_20191121_164146.jpg differ diff --git a/baidu-salon/img/IMG_20191121_164901.jpg b/baidu-salon/img/IMG_20191121_164901.jpg new file mode 100644 index 0000000..e0cfbad Binary files /dev/null and b/baidu-salon/img/IMG_20191121_164901.jpg differ diff --git a/baidu-salon/img/IMG_20191121_164903.jpg b/baidu-salon/img/IMG_20191121_164903.jpg new file mode 100644 index 0000000..a44944a Binary files /dev/null and b/baidu-salon/img/IMG_20191121_164903.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165005.jpg b/baidu-salon/img/IMG_20191121_165005.jpg new file mode 100644 index 0000000..998d2d0 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165005.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165006.jpg b/baidu-salon/img/IMG_20191121_165006.jpg new file mode 100644 index 0000000..36c120f Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165006.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165606.jpg b/baidu-salon/img/IMG_20191121_165606.jpg new file mode 100644 index 0000000..582c633 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165606.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165610.jpg b/baidu-salon/img/IMG_20191121_165610.jpg new file mode 100644 index 0000000..b62aac9 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165610.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165707.jpg b/baidu-salon/img/IMG_20191121_165707.jpg new file mode 100644 index 0000000..1c22a67 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165707.jpg differ diff --git a/baidu-salon/img/IMG_20191121_165710.jpg b/baidu-salon/img/IMG_20191121_165710.jpg new file mode 100644 index 0000000..16c294e Binary files /dev/null and b/baidu-salon/img/IMG_20191121_165710.jpg differ diff --git a/baidu-salon/img/IMG_20191121_170117.jpg b/baidu-salon/img/IMG_20191121_170117.jpg new file mode 100644 index 0000000..9eec89e Binary files /dev/null and b/baidu-salon/img/IMG_20191121_170117.jpg differ diff --git a/baidu-salon/img/IMG_20191121_170120.jpg b/baidu-salon/img/IMG_20191121_170120.jpg new file mode 100644 index 0000000..f3d9e4c Binary files /dev/null and b/baidu-salon/img/IMG_20191121_170120.jpg differ diff --git a/baidu-salon/img/IMG_20191121_170139.jpg b/baidu-salon/img/IMG_20191121_170139.jpg new file mode 100644 index 0000000..0ba0a4d Binary files /dev/null and b/baidu-salon/img/IMG_20191121_170139.jpg differ diff --git a/baidu-salon/img/IMG_20191121_171939.jpg b/baidu-salon/img/IMG_20191121_171939.jpg new file mode 100644 index 0000000..6e3c089 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_171939.jpg differ diff --git a/baidu-salon/img/IMG_20191121_171941.jpg b/baidu-salon/img/IMG_20191121_171941.jpg new file mode 100644 index 0000000..07c7c98 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_171941.jpg differ diff --git a/baidu-salon/img/IMG_20191121_172338.jpg b/baidu-salon/img/IMG_20191121_172338.jpg new file mode 100644 index 0000000..d65e5bb Binary files /dev/null and b/baidu-salon/img/IMG_20191121_172338.jpg differ diff --git a/baidu-salon/img/IMG_20191121_172340.jpg b/baidu-salon/img/IMG_20191121_172340.jpg new file mode 100644 index 0000000..751a932 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_172340.jpg differ diff --git a/baidu-salon/img/IMG_20191121_172510.jpg b/baidu-salon/img/IMG_20191121_172510.jpg new file mode 100644 index 0000000..cac0d86 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_172510.jpg differ diff --git a/baidu-salon/img/IMG_20191121_173428.jpg b/baidu-salon/img/IMG_20191121_173428.jpg new file mode 100644 index 0000000..df346b1 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_173428.jpg differ diff --git a/baidu-salon/img/IMG_20191121_173637.jpg b/baidu-salon/img/IMG_20191121_173637.jpg new file mode 100644 index 0000000..5f8c7db Binary files /dev/null and b/baidu-salon/img/IMG_20191121_173637.jpg differ diff --git a/baidu-salon/img/IMG_20191121_173639.jpg b/baidu-salon/img/IMG_20191121_173639.jpg new file mode 100644 index 0000000..ab46de4 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_173639.jpg differ diff --git a/baidu-salon/img/IMG_20191121_173711.jpg b/baidu-salon/img/IMG_20191121_173711.jpg new file mode 100644 index 0000000..392ec91 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_173711.jpg differ diff --git a/baidu-salon/img/IMG_20191121_173713.jpg b/baidu-salon/img/IMG_20191121_173713.jpg new file mode 100644 index 0000000..7f46642 Binary files /dev/null and b/baidu-salon/img/IMG_20191121_173713.jpg differ diff --git "a/baidu-salon/\347\231\276\345\272\246\346\262\231\351\276\231\347\240\224\350\256\250\344\274\232\345\206\205\345\256\271\346\246\202\350\246\201.md" "b/baidu-salon/\347\231\276\345\272\246\346\262\231\351\276\231\347\240\224\350\256\250\344\274\232\345\206\205\345\256\271\346\246\202\350\246\201.md" new file mode 100644 index 0000000..a318565 --- /dev/null +++ "b/baidu-salon/\347\231\276\345\272\246\346\262\231\351\276\231\347\240\224\350\256\250\344\274\232\345\206\205\345\256\271\346\246\202\350\246\201.md" @@ -0,0 +1,29 @@ +# 百度沙龙研讨会内容概要 + +## 一、概要 +主要分为两大块,一块是百度外部邀请的宇信、浦发、Intel、火币介绍他们的应用的相关案例。第二块是百度自己在区块链和云平台结合的方式和相关案例。干货还是不少的。 +## 二、内容 +整个研讨基本围绕两部分展开,一部分是金融,这是重点,几乎到会的没有一个不在这个方向上提出自己的设想和方案。第二个是就是百度天链的整体架构,其实百度天链就是一个基于区块链的智能调度云平台。兼容以太坊、Quarum、Fabric和百度自己的超级链及智能云自有链(基于cosmos)。 +
+在这其中Intel基于SGX专门针对Fabric开源了阿沃隆可信计算框架。这对于我们做可信计算多了一个借鉴。百度的mesaTEE+SGX可能就是借鉴或者基于这个来做的。 +## 三、主要案例 +1、比较典型的例子 +
+金融方向上主要提出了两个例子: +
+一个是小额信贷,这里不赘述。 +
+第二个是类似于可信环境信息传递。它进行了比较详细的说明,比如有A,B,C三个银行,经常会有这种情况,客户在A行有帐户,他要贷款,从A行打印了相关信息,云B行咨询,但最后在c行贷了款。这其中的信息流转一个是麻烦,另外一个,可能就把A行的一些有价值的信息泄露给了其它行(当然,这个是合法泄露的)。这时候儿如果使用可信区块链环境,就可以A行开一个查询接口,其它行可以查询相关的信息,但是具体的信息内容不可见(个人理解是不是类似于同态加密,你只知道客户的评分等级,却无法知道其具体的内容)。然后A行可以靠这个服务论笔或其它形式来收费。 +
+ +2、传统的例子 +
+就是类似于积分、存证、溯源、版权等等。不展开 +
+ +3、DID小程序 +
+百度基于天链做了一个分布式的个人ID认证系统。大家如果有兴趣可以去看看。 + +## 四、资料 +图片在img文件夹内。 diff --git "a/bitcoin/Qt\351\222\261\345\214\205\347\232\204UI\347\261\273\345\233\276.md" "b/bitcoin/Qt\351\222\261\345\214\205\347\232\204UI\347\261\273\345\233\276.md" new file mode 100644 index 0000000..0e90d18 --- /dev/null +++ "b/bitcoin/Qt\351\222\261\345\214\205\347\232\204UI\347\261\273\345\233\276.md" @@ -0,0 +1,9 @@ +# Qt钱包的UI类图 +QT的钱包部分划成两部分来画类图,这样会更清晰一些,先处理UI的部分,钱包整体可以大致划分为三个部分:闪屏和相关介绍;主程序界面;后台操作接口。 +
+先把类图画上: +
+![钱包类图UI](img/wallet-ui.png) +
+整体上来说,界面上有四个部分,即 **概况,发送,接收和交易记录** 。其使用一个QStackedWidget控件来实现四个页面的加载。 +然后再通过一个公用的WalletView来实现实际界面的生成和转换。其中一些细节性的对话框只是列了出来,没有详细展开。 diff --git a/bitcoin/img/appinit2.png b/bitcoin/img/appinit2.png new file mode 100644 index 0000000..be51c54 Binary files /dev/null and b/bitcoin/img/appinit2.png differ diff --git a/bitcoin/img/bitcoin-node.png b/bitcoin/img/bitcoin-node.png new file mode 100644 index 0000000..88e5660 Binary files /dev/null and b/bitcoin/img/bitcoin-node.png differ diff --git a/bitcoin/img/bloom.png b/bitcoin/img/bloom.png new file mode 100644 index 0000000..cfc3a67 Binary files /dev/null and b/bitcoin/img/bloom.png differ diff --git a/bitcoin/img/fee-cus.png b/bitcoin/img/fee-cus.png new file mode 100644 index 0000000..37207e1 Binary files /dev/null and b/bitcoin/img/fee-cus.png differ diff --git a/bitcoin/img/httpRPC.png b/bitcoin/img/httpRPC.png new file mode 100644 index 0000000..466ee30 Binary files /dev/null and b/bitcoin/img/httpRPC.png differ diff --git a/bitcoin/img/init.png b/bitcoin/img/init.png new file mode 100644 index 0000000..c46fdc5 Binary files /dev/null and b/bitcoin/img/init.png differ diff --git a/bitcoin/img/mnemonic.png b/bitcoin/img/mnemonic.png new file mode 100644 index 0000000..8b6cf05 Binary files /dev/null and b/bitcoin/img/mnemonic.png differ diff --git a/bitcoin/img/node-detail.png b/bitcoin/img/node-detail.png new file mode 100644 index 0000000..4061873 Binary files /dev/null and b/bitcoin/img/node-detail.png differ diff --git a/bitcoin/img/qt-wallet0.png b/bitcoin/img/qt-wallet0.png new file mode 100644 index 0000000..4939469 Binary files /dev/null and b/bitcoin/img/qt-wallet0.png differ diff --git a/bitcoin/img/qt-wallet1.png b/bitcoin/img/qt-wallet1.png new file mode 100644 index 0000000..d669953 Binary files /dev/null and b/bitcoin/img/qt-wallet1.png differ diff --git a/bitcoin/img/seedtokey.png b/bitcoin/img/seedtokey.png new file mode 100644 index 0000000..d766b88 Binary files /dev/null and b/bitcoin/img/seedtokey.png differ diff --git a/bitcoin/img/sendcoin-b.png b/bitcoin/img/sendcoin-b.png new file mode 100644 index 0000000..6a0a13e Binary files /dev/null and b/bitcoin/img/sendcoin-b.png differ diff --git a/bitcoin/img/transaction-1.png b/bitcoin/img/transaction-1.png new file mode 100644 index 0000000..6af1790 Binary files /dev/null and b/bitcoin/img/transaction-1.png differ diff --git a/bitcoin/img/transaction-mempools.png b/bitcoin/img/transaction-mempools.png new file mode 100644 index 0000000..45fd476 Binary files /dev/null and b/bitcoin/img/transaction-mempools.png differ diff --git a/bitcoin/img/transfer.png b/bitcoin/img/transfer.png new file mode 100644 index 0000000..9cb8628 Binary files /dev/null and b/bitcoin/img/transfer.png differ diff --git a/bitcoin/img/ui-1.png b/bitcoin/img/ui-1.png new file mode 100644 index 0000000..4939469 Binary files /dev/null and b/bitcoin/img/ui-1.png differ diff --git a/bitcoin/img/wallet-ui.png b/bitcoin/img/wallet-ui.png new file mode 100644 index 0000000..9e22a91 Binary files /dev/null and b/bitcoin/img/wallet-ui.png differ diff --git "a/bitcoin/\344\272\244\346\230\223\346\272\220\347\240\201\345\210\206\346\236\220.pdf" "b/bitcoin/\344\272\244\346\230\223\346\272\220\347\240\201\345\210\206\346\236\220.pdf" new file mode 100644 index 0000000..9c6bb85 Binary files /dev/null and "b/bitcoin/\344\272\244\346\230\223\346\272\220\347\240\201\345\210\206\346\236\220.pdf" differ diff --git "a/bitcoin/\344\272\244\346\230\223\347\232\204\347\261\273\345\233\276.md" "b/bitcoin/\344\272\244\346\230\223\347\232\204\347\261\273\345\233\276.md" new file mode 100644 index 0000000..f8571f9 --- /dev/null +++ "b/bitcoin/\344\272\244\346\230\223\347\232\204\347\261\273\345\233\276.md" @@ -0,0 +1,12 @@ +# 交易的类图 +
+一、交易的类图 +
+ +![transaction-class](img/transaction-1.png) +
+二、交易内存池的类图 +
+ +![trans-mempool](img/transaction-mempools.png) +
diff --git "a/bitcoin/\345\205\254\351\223\276\347\232\204\346\236\266\346\236\204.xmind" "b/bitcoin/\345\205\254\351\223\276\347\232\204\346\236\266\346\236\204.xmind" new file mode 100644 index 0000000..5572b14 Binary files /dev/null and "b/bitcoin/\345\205\254\351\223\276\347\232\204\346\236\266\346\236\204.xmind" differ diff --git "a/bitcoin/\346\257\224\347\211\271\345\270\201\344\270\255\347\232\204 HTTP \346\234\215\345\212\241.md" "b/bitcoin/\346\257\224\347\211\271\345\270\201\344\270\255\347\232\204 HTTP \346\234\215\345\212\241.md" new file mode 100644 index 0000000..1d43e48 --- /dev/null +++ "b/bitcoin/\346\257\224\347\211\271\345\270\201\344\270\255\347\232\204 HTTP \346\234\215\345\212\241.md" @@ -0,0 +1,299 @@ + +### 比特币中的 HTTP 服务 +--- +bitcoind 通过 http 服务向外界提供了一系列基于 json-rpc 规范的 rpc 命令,这些命令涉及挖矿、交易、链信息查询等。本篇文章简要介绍其具体实现。通过对这些 rpc 命令的了解有助于更好的使用 bitcoind 提供的服务,这里还涉及到一个典型的生产者消费者模型。 + +#### 一、提供的 rpc 命令 +源码中把每一个 rpc 命令都封装到类 CRPCCommand 中,该类包含命令的名字以及处理该命令时调用的函数等,并且用一个 CRPCTable 类来管理所有的 CRPCCommand,对于每一个调用 rpc 命令的请求,CRPCTable 会根据请求的命令查找其管理的 CRPCCommand,执行对应的处理函数,代码如下: +```cpp +typedef UniValue(*rpcfn_type)(const JSONRPCRequest& jsonRequest); // 定义 rpc 命令调用的函数类型 + +class CRPCCommand +{ +public: + std::string category; // 命令所属范围 + std::string name; // 命令名字 + rpcfn_type actor; // 该命令调用的函数 + std::vector argNames; +}; + +class CRPCTable +{ +private: + std::map mapCommands; //维护所有 rpc 命令 +public: + CRPCTable(); + const CRPCCommand* operator[](const std::string& name) const; + std::string help(const std::string& name, const JSONRPCRequest& helpreq) const; + UniValue execute(const JSONRPCRequest &request) const; // 执行命令 + std::vector listCommands() const; + bool appendCommand(const std::string& name, const CRPCCommand* pcmd); // 添加命令 +}; +``` +server.cpp 文件中定义了一个 CRPCTable 类型的对象 tableRPC,bitcoind 在启动时会把所有的相关 rpc 命令通过 appendCommand 函数添加到 tableRPC。 + +#### 二、不同的 http 请求路径对应的处理函数 +类 HTTPPathHandler 中含有 http 请求的路径以及对应的处理函数,这和 CRPCCommand 非常相似,pathHandlers 是一个这种类型的 vector,可通过 RegisterHTTPHandler 函数向 pathHandlers 中添加 HTTPPathHandler 类型的元素。 +```cpp +std::vector pathHandlers; +struct HTTPPathHandler +{ + ...... + std::string prefix; // 请求路径 + bool exactMatch; + HTTPRequestHandler handler; // 处理函数 +}; + +void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler) +{ + LogPrint(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch); + pathHandlers.push_back(HTTPPathHandler(prefix, exactMatch, handler)); +} +// 在 StartHTTPRPC() 函数中向 pathHandlers 添加元素。 +bool StartHTTPRPC() +{ +...... + RegisterHTTPHandler("/", true, HTTPReq_JSONRPC); +#ifdef ENABLE_WALLET + // ifdef can be removed once we switch to better endpoint support and API versioning + RegisterHTTPHandler("/wallet/", false, HTTPReq_JSONRPC); +#endif +...... +} +``` +可见针对不同的路径这里都是调用 HTTPReq_JSONRPC 函数来处理 http 请求。由于客户端要把想调用的 rpc 命令以 json 格式放在 http 请求的 body 体中,所以这里的 http 服务只支持 post 请求。HTTPReq_JSONRPC 把 http 请求的数据解析到一个类 JSONRPCRequest 的对象 jreq 中,jreq 中含有 http 请求的路径以及从 body 体中解析出的 rpc 请求及参数,然后以 jreq 为参数去 tableRPC 中执行 rpc 命令对应的函数。部分代码如下: +```cpp +static bool HTTPReq_JSONRPC(HTTPRequest* req, const std::string &) +{ + // JSONRPC handles only POST + if (req->GetRequestMethod() != HTTPRequest::POST) { + req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests"); + return false; + } +...... + JSONRPCRequest jreq; +...... + try { + // Parse request + UniValue valRequest; + if (!valRequest.read(req->ReadBody())) + throw JSONRPCError(RPC_PARSE_ERROR, "Parse error"); + + // Set the URI + jreq.URI = req->GetURI(); + + std::string strReply; + // singleton request + if (valRequest.isObject()) { + jreq.parse(valRequest); + + UniValue result = tableRPC.execute(jreq); + + // Send reply + strReply = JSONRPCReply(result, NullUniValue, jreq.id); + + // array of requests + } else if (valRequest.isArray()) + strReply = JSONRPCExecBatch(jreq, valRequest.get_array()); + else + throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error"); + + req->WriteHeader("Content-Type", "application/json"); + req->WriteReply(HTTP_OK, strReply); + } catch (const UniValue& objError) { + JSONErrorReply(req, objError, jreq.id); + return false; + } catch (const std::exception& e) { + JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id); + return false; + } + return true; +} +``` + +#### 三、借助 libevent 来提供 http 服务 +bitcoind 没有自己去做底层的网络编程,而是借助于比较成熟的网络编程库 libevent,这样用少量代码就可以实现自己的 http 服务,不需要考虑复杂的网络连接请求等问题,以下是部分代码。 +```cpp +bool InitHTTPServer() +{ + if (!InitHTTPAllowList()) // 设置允许的客户端地址 + return false; +...... + raii_event_base base_ctr = obtain_event_base(); + + /* Create a new evhttp object to handle requests. */ + raii_evhttp http_ctr = obtain_evhttp(base_ctr.get()); + struct evhttp* http = http_ctr.get(); + if (!http) { + LogPrintf("couldn't create evhttp. Exiting.\n"); + return false; + } + + evhttp_set_timeout(http, gArgs.GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)); + evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE); + evhttp_set_max_body_size(http, MAX_SIZE); + evhttp_set_gencb(http, http_request_cb, nullptr); // 设置处理 http 请求的回调函数 +...... + workQueue = new WorkQueue(workQueueDepth); +...... + return true; +} + +static void http_request_cb(struct evhttp_request* req, void* arg) +{ +...... + std::unique_ptr hreq(new HTTPRequest(req)); +...... + // Early address-based allow check + if (!ClientAllowed(hreq->GetPeer())) { + hreq->WriteReply(HTTP_FORBIDDEN); + return; + } +...... + // Find registered handler for prefix + std::string strURI = hreq->GetURI(); + std::string path; + std::vector::const_iterator i = pathHandlers.begin(); + std::vector::const_iterator iend = pathHandlers.end(); + for (; i != iend; ++i) { + bool match = false; + if (i->exactMatch) + match = (strURI == i->prefix); + else + match = (strURI.substr(0, i->prefix.size()) == i->prefix); + if (match) { + path = strURI.substr(i->prefix.size()); + break; + } + } + + // Dispatch to worker thread + if (i != iend) { + std::unique_ptr item(new HTTPWorkItem(std::move(hreq), path, i->handler)); + assert(workQueue); + if (workQueue->Enqueue(item.get())) + item.release(); /* if true, queue took ownership */ + else { + LogPrintf("WARNING: request rejected because http work queue depth exceeded, it can be increased with the -rpcworkqueue= setting\n"); + item->req->WriteReply(HTTP_INTERNAL, "Work queue depth exceeded"); + } + } else { + hreq->WriteReply(HTTP_NOTFOUND); + } +} +``` +函数 http_request_cb 负责处理 http 请求, 它首先根据请求路径到 pathHandlers 中查找对应的处理函数(这里都是 HTTPReq_JSONRPC),然后将找到的函数封装到 HTTPWorkItem 类型的对象 item 中,并将 item 放入 workQueue 中,由其它线程来进行处理。这里就涉及到一个典型的生产者消费者模型。 + +#### 四、典型的生产者消费者模型 +类模板 WorkQueue 定义了一个生产者消费者模型 +```cpp +/** HTTP request work item */ +class HTTPWorkItem final : public HTTPClosure +{ +public: +...... + void operator()() override + { + func(req.get(), path); + } + + std::unique_ptr req; + +private: + std::string path; + HTTPRequestHandler func; +}; + +/** Simple work queue for distributing work over multiple threads. + * Work items are simply callable objects. + */ +template +class WorkQueue +{ +private: + /** Mutex protects entire object */ + std::mutex cs; + std::condition_variable cond; + std::deque> queue; + bool running; + size_t maxDepth; + +public: +...... + /** Enqueue a work item */ + bool Enqueue(WorkItem* item) + { + std::unique_lock lock(cs); + if (queue.size() >= maxDepth) { + return false; + } + queue.emplace_back(std::unique_ptr(item)); + cond.notify_one(); + return true; + } + /** Thread function */ + void Run() + { + while (true) { + std::unique_ptr i; + { + std::unique_lock lock(cs); + while (running && queue.empty()) + cond.wait(lock); + if (!running) + break; + i = std::move(queue.front()); + queue.pop_front(); + } + (*i)(); + } + } +...... +}; + +static WorkQueue* workQueue = nullptr; // 以 HTTPClosure 为模板参数定义变量 workQueue。 +``` + +生产者:http_request_cb 根据 http 请求路径从 pathHandlers 中找出对应函数(这里都是 HTTPReq_JSONRPC),然后把该函数封装到 WorkItem, 然后调用 workQueue 的成员函数 Enqueue 将 WorkItem 放入 workQueue 中,而 workQueue 封装了一个元素为 WorkItem 类型的队列。 +消费者:bitcoind 在启动时创建了 rpcThreads 个消费者线程,这些线程(HTTPWorkQueueRun) 调用 WorkQueue 的 Run() 方法从 workQueue 中取出 WorkItem 并调用其函数调用运算符(执行 HTTPReq_JSONRPC 函数)。 +这里使用 C++ 中提供的类 std::thread 来创建线程,代码如下所示: +```cpp +/** Simple wrapper to set thread name and run work queue */ +static void HTTPWorkQueueRun(WorkQueue* queue) +{ + RenameThread("bitcoin-httpworker"); + queue->Run(); +} + +static std::vector g_thread_http_workers; + +bool StartHTTPServer() +{ + LogPrint(BCLog::HTTP, "Starting HTTP server\n"); + int rpcThreads = std::max((long)gArgs.GetArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L); + LogPrintf("HTTP: starting %d worker threads\n", rpcThreads); + std::packaged_task task(ThreadHTTP); + threadResult = task.get_future(); + threadHTTP = std::thread(std::move(task), eventBase, eventHTTP); + + for (int i = 0; i < rpcThreads; i++) { + g_thread_http_workers.emplace_back(HTTPWorkQueueRun, workQueue); //启动消费者线程 + } + return true; +} + +/** Event dispatcher thread */ +static bool ThreadHTTP(struct event_base* base, struct evhttp* http) +{ + RenameThread("bitcoin-http"); + LogPrint(BCLog::HTTP, "Entering http event loop\n"); + event_base_dispatch(base); // 调用 libevent 接口处理 http 连接请求 + // Event loop will be interrupted by InterruptHTTPServer() + LogPrint(BCLog::HTTP, "Exited http event loop\n"); + return event_base_got_break(base) == 0; +} +``` + +![流程图](img/httpRPC.png) + +注:具体的 rpc 命令以及以及其处理过程在 bitcoin/src/rpc 目录下的 rawtransaction.cpp、mining.cpp、blockchain.cpp、misc.cpp、net.cpp 文件中。 diff --git "a/bitcoin/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\225\264\344\275\223\346\265\201\347\250\213\345\233\276\345\217\212\350\257\264\346\230\216.md" "b/bitcoin/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\225\264\344\275\223\346\265\201\347\250\213\345\233\276\345\217\212\350\257\264\346\230\216.md" new file mode 100644 index 0000000..174cb17 --- /dev/null +++ "b/bitcoin/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\225\264\344\275\223\346\265\201\347\250\213\345\233\276\345\217\212\350\257\264\346\230\216.md" @@ -0,0 +1,11 @@ +# 比特币源码的整体流程图及说明 +*** +## 1.整体流程图如下: + +### bitcoind主程序流程 + +![Appinit](img/init.png) + +### AppInit2流程 + +![AppInit2](img/appinit2.png) diff --git "a/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\265\201\347\250\213\345\210\206\346\236\220\350\257\264\346\230\216.txt" "b/bitcoin/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\265\201\347\250\213\345\210\206\346\236\220\350\257\264\346\230\216.txt" similarity index 100% rename from "\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\265\201\347\250\213\345\210\206\346\236\220\350\257\264\346\230\216.txt" rename to "bitcoin/\346\257\224\347\211\271\345\270\201\346\272\220\347\240\201\347\232\204\346\265\201\347\250\213\345\210\206\346\236\220\350\257\264\346\230\216.txt" diff --git "a/bitcoin/\346\257\224\347\211\271\345\270\201\351\222\261\345\214\205\345\257\206\351\222\245\347\256\241\347\220\206\346\246\202\350\247\210.md" "b/bitcoin/\346\257\224\347\211\271\345\270\201\351\222\261\345\214\205\345\257\206\351\222\245\347\256\241\347\220\206\346\246\202\350\247\210.md" new file mode 100644 index 0000000..927f247 --- /dev/null +++ "b/bitcoin/\346\257\224\347\211\271\345\270\201\351\222\261\345\214\205\345\257\206\351\222\245\347\256\241\347\220\206\346\246\202\350\247\210.md" @@ -0,0 +1,122 @@ +# 比特币钱包密钥管理概览 + +钱包在整个区块链技术领域是比较独立的,和其他部分交互较少;本文介绍现阶段(2018年5月)开发一款钱包密钥管理需要满足的功能和事实标准。 + +## 1. 前置知识 + +这部分主要介绍开发钱包之前需要知道的几个知识点。 + +### (1). 比特币私钥,公钥和地址之前的关系 + +比特币中使用椭圆曲线密码学做为公私钥的核心算法(所采用的椭圆曲线是 [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1)),私钥本质是一个大的随机数,保密存储,用于签名交易,从而证明 UTXO 的所有权。公钥为椭圆曲线上一点坐标,通过私钥和所在曲线计算得来,这个计算是不可逆的,即不存在一种计算方式从公钥算出所在私钥。通常这里私钥是 32Byte,而公钥是 33Byte(压缩公钥格式),比特币地址本质是公钥的哈希结果;这里使用的哈希是 sha256 + ripemd160,即常说的双哈希,或者又叫哈希160,地址通常表示为哈希结果的 Base58Check 编码方式。 + +### (2). 钱包概述 + +和生活实际的认知有所不同,比特币钱包并不存储数字货币,钱包的基础是存储私钥,用私钥对交易进行签名,用于证明可以花费掉一定数额的数字货币。 + +一个完整的交易(转账)过程包括创建交易、签名交易、广播交易,其中创建交易和广播交易不需要访问私钥,创建交易和签名交易不需要访问网络;所以钱包又有硬件钱包、冷钱包、热钱包之分,硬件钱包是将私钥存储在独立硬件上,存储的私钥无法通过外部访问,只提供对交易进行签名的接口,所以安全性上比较高,同时也需要独立购买硬件;冷钱包相较于硬件钱包来说,私钥存储在无法联网的机器上,离线对交易进行签名;热钱包整个签名过程都是在网络环境下,使用起来比较方便; + +钱包从另一层面上来说又有全节点钱包和轻钱包之分,因为交易最终是需要广播出去的,而区块链又是分布式去中心化的,所以钱包广播需要节点进行参与,全节点钱包和轻钱包都是钱包本身作为一个节点,然后向其他节点广播交易,是一个完全去中心化的结构,全节点和轻节点只是节点类型不同;还有一类钱包本身不作为节点,而是多个钱包连接一个中心化的服务器,这个中心化的服务器作为一个节点参与广播,而钱包只包含创建交易和签名交易的功能,签名完的交易通过其他协议(例如 HTTPS)传输给中心化服务器,中心化服务器将交易进行广播;而中心化的服务器也能提供其他类似于实时汇率等功能;Coinbase,Copay,Blockchain.info 等钱包都是这样的一个 CS 架构; + +还有一类交易所的钱包,例如火币钱包,钱包本身并不存储私钥,所有的操作和中心化服务器进行交互,至于中心化服务器中是否会为每个用户创建私钥进行管理,是一个黑盒不得而知; + +## 2. 实现钱包密钥管理的几个标准 + +现阶段,对于开发钱包来说,有几个事实标准需要遵守和实现,而且现有市面上钱包都是实现这样的几个标准的 + +1. [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki): 分层确定性钱包(HD钱包)规范; +2. [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki): 私钥与助记词(mnemonic code)相互转换的规范; +3. [BIP-0043](https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki): 多用途 HD 钱包规范; +4. [BIP-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki): 多账户多币种 HD 钱包规范; + +这几个规范基本现有的钱包都会实现,其中 BIP-0032、BIP-0038、BIP-0039 之间相互独立,处于钱包实现的不同流程;BIP-0043 是在 BIP-0032 上制定的规范,BIP-0044 又基于 BIP-0043 而制定了更详细的一个规范;和 BIP-0044 同类型的还有 BIP-0045、BIP-0047、BIP-0049 等几种规范; + +## 3. BIP-0032 + +比特币建议的同一个地址使用一次,相较于普通的 `私钥 -> 公钥 -> 地址` 的这一关系,钱包需要维护多个地址,所以钱包有两个方向可以做,一种是存储独立的多个私钥,从而对应多个地址,这也就是非确定性钱包;另一种思路是一个种子(Seed)可以扩展产生多个私钥,每个私钥又有自己的对应公钥、地址,这就是确定性钱包;BIP-0032 制定了一种具有分层的确定性钱包规范。 + +使用非确定性钱包有两个缺点,每个地址对应一个私钥,需要备份的私钥比较多,丢失其中一个私钥,也会损失那部分比特币;私钥在使用过程中不断生成,也需要不时进行备份。 + +HD 钱包的种子本质还是随机数,或者从别的规范产生的一个数字,最常见是通过 BIP-0039 的助记词产生的; + +首先,种子通过 HMAC-SHA512(Key="Bitcoin seed", Data=Seed) 哈希函数产生一个 64Byte 的主密钥(Key 是一个确定的字符串 "Bitcoin seed"),主密钥分割成 32Byte 的两部分,左侧用作主私钥,右侧用作衍生子密钥,称为链码(chain code), 左侧的主私钥可以计算出公钥,从而计算出地址来正常使用;主密钥和链码,再结合子密钥索引(索引是一个 4Byte 序号),通过 HMAC-SHA512 衍生出子密钥,这里主要分为三种类型: + +#### (1). 父级私钥 --> 子级私钥 + +这里还是使用 HMAC-SHA512 哈希函数,父级的 ChainCode 作为哈希函数的 Key,父级私钥计算得到的公钥(33Byte)和索引拼起来用作哈希函数的 Data,哈希函数同样产生一个 64Byte 的子密钥,同样分割为 32Byte 的两部分,右侧的 ChainCode 用作于子链衍生自己的子链;左侧通过和父级私钥进行数字相加得到子级私钥;这里子级私钥有个椭圆曲线和是否为零的校验;校验通过则是一个合法的子级私钥,不合法则会跳过当前索引。 + +#### (2). 父级公钥 --> 子级公钥 + +这里 HMAC-SHA512 的计算和 (1) 中一致,只是这里父级公钥直接参与计算,而不是从私钥推算;同样哈希函数分为两部分,右侧为 ChainCode,左侧的 32Byte 通过椭圆曲线计算出公钥,再和父级公钥通过椭圆曲线的点坐标加法从而得到子级的公钥; + +所以这里在使用中就可以只通过父级公钥和链码在不安全环境下衍生需要的子级公钥,全程不需要私钥的参与;这里有个需要明白的,得到的子级公钥在使用中需要私钥来解密,通过 (1) 中同样的索引,计算出来的子级私钥和这里计算的子级公钥就是一个公私钥对;因为这里的椭圆曲线是个可交换群,满足交换律和结合律,而椭圆曲线中私钥计算公钥是一个乘法(PubKey = PrevKey * P, P 是特定椭圆曲线上常数),所以这里满足 ParentPrevKey * P + LeftBytes * P = (ParentPrevKey + LeftBytes) * P,其中 LeftBytes * P 即为左侧的 32Byte 通过椭圆曲线计算的公钥; + +#### (3). 父级私钥 --> 子级公钥 + +父级私钥得到子级公钥有两种途径,一种途径是先经过 (1),衍生出子级私钥从而计算出子级公钥;另一种途径先计算出父级公钥,再通过 (2) 衍生出子级公钥; + +### 强化衍生 + +前面提到的父级公钥和链码合起来称为扩展公钥,在不安全环境下来衍生子公钥,但一旦子私钥被泄漏,和扩展公钥中的链码就可以衍生它的所有子私钥,另外子私钥和父级链码有推断出父级私钥的风险,所以又引入了强化衍生的概念。 + +强化衍生只能从父级私钥衍生子级私钥,从而得到子级公钥,而不能做如上 (2) 中的公钥衍生,具体的衍生过程和 (1) 中 HMAC-SHA512 有细微区别,不同于 (1) 中的父级私钥计算得到的公钥(33Byte),强化衍生通过 0x00 这样 1Byte 拼上私钥和索引作为 Data,其他衍生步骤一致。强化衍生不会将父级链码暴露到不安全的环境下,所以相较于正常衍生更安全些。 + +### 子密钥的索引号码和路径表示 + +索引的范围有 2^32 个,其中前 2^31 表示正常衍生,通常表示为索引序号即可;后 2^31 表示强化衍生,强化衍生序号也是从 0 开始,会在序号后添加撇号表示强化衍生,例如 `0x80000000` 为第一个强化衍生的序号,通常表示为 `0'`,钱包中有 `i'=2^31+i` 这样的关系。 + +子密钥的路径通过 `/` 分割的索引表示;前缀有 `m` 和 `M` 之分,`m` 表示衍生的子私钥,`M` 表示衍生的子公钥。例如 `m/5/4'/0` 表示子私钥 `m/5/4'` 的第 0 个正常衍生私钥,`m/5/4'` 则是子私钥 `m/5` 的第 4 个强化衍生的子私钥,`m/5` 则是主私钥第 5 个正常衍生的子私钥。 + +## 4. BIP-0039 + +助记词相较于随机数字的表示更容易地被阅读和正确抄写,可以让使用者复制钱包更容易一些。助记词主要使用在钱包进行备份和转移的时候。 + +助记词生成的基本流程是: + +1. 生成 128bits 的随机数,称为熵(以128bits为例); +1. 计算熵 checksum: 将熵进行 SHA256 哈希,取出前 4bits 用作校验码;(校验码长度 = 数据总长度 / 32); +2. 将原始熵和 checksum 拼接为 132bits 长度; +3. 将 132bits 长度按照 11bits 分割为 12 份; +4. 每 11bits 对应为 2048 长度的单词表中的一个单词; + +这 12 个单词就是熵对应的助记词,单词表可以从 [单词列表](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md) 这里获取到; + +其中熵长度不一样,对应的助记词个数也不一样;熵长度可取值范围如下: + +熵长度 | checksum 长度 | 助记词长度 +------- | ------------- | ---------- +128 | 4 | 12 +160 | 5 | 15 +192 | 6 | 18 +224 | 7 | 21 +256 | 8 | 24 + +### 从助记词生成种子 + +这里使用称为 PBKDF2 的算法来从助记词生成种子,PBKDF2 函数接收两个参数:生成的助记词字符序列;第二个参数还有用于密码加密的盐,盐(salt)的参数为字符串 "mnemonic" 和用户可选密码拼起来的字符串,没有可选密码则 salt 参数就是 "mnemonic"。 + +PBKDF2 使用循环 2048 次的 HMAC-SHA512 函数来实现,最后返回一个 512 bits 长度的种子;这里生成的种子可以用作上面 BIP-0032 的种子。 + +## BIP-0043 + +由于 BIP-0032 层级可以无限扩展,每层又可以有很多子密钥,这样定义过于松散,不同钱包可能会定义自己的生成路径规则,所以 BIP-0043 定义了分层钱包层级如何使用的规范。 + +BIP-0043 规定了 `m/purpose'/` 第一级路径作为目的,即定义路径所遵循的规范,这样防止采用不同规范的路径的冲突;例如 `0'` 为 BIP-32 的默认钱包,`44'` 为遵守 BIP-0044 规范所定义的前缀;这里规范使用了强化衍生。 + +## BIP-0044 + +BIP-0044 在 BIP-0043 的基础上更详细的定义了一种路径规范:`m/purpose'/coin_type'/account'/change/address_index`,这里 purpose 还是遵循 BIP-0043 的规范,在 BIP-0044 中就是固定值 `44'`;coin_type 定义了可以支持的数字货币种类,已经注册的有比特币(`0'`),比特币测试网(`1'`),这里为实现多币种钱包提供了支持;account 定义了多账户的支持,依次从 0 开始,purpose、coin_type、account 都是强化衍生;change 定义了外部地址(`0`)和找零地址(`1`),外部地址一般用于接收转账等,找零地址如字面意思,用于接收转账的找零;address_index 就是从 0 开始的地址序号。 + +BIP-0045 定义了另外一种形式的路径规范,有兴趣可以了解下; + +## 隔离见证(Segwit) + +对于现在的钱包来说,部分实现了 [BIP-0141](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki) 的隔离见证。简单来说,比特币扩容和区块大小 1M 的限制相矛盾,有了修改块大小(BCH方案)和隔离见证两种方式,隔离见证就是将脚本签名(scriptSig)这部分结构从区块中拿出来单独存储,计算块大小就不会计算这部分了,从而一个块中容纳更多的交易;这部分脚本签名仍然在区块链中,跟随着交易一起验证。 + +现在支持隔离见证的钱包有 [copay](https://copay.io/)。 + +## 结束语 + +钱包除了上面介绍的公私钥管理以外,一般还需要有创建交易和对创建的交易进行签名并将交易发送到区块链并由矿工确认,交易创建和签名又是一块独立完整的内容,这里就不再展开了。 + +比特币钱包中的密钥管理相对来说比较简单一些,相对整体结构来说也比较独立。而钱包客户端又是用户和区块链交互的桥梁,所以实现钱包密钥管理的统一规范还是很重要。 diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\270\200.docx" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\270\200.docx" new file mode 100644 index 0000000..4a9628e Binary files /dev/null and "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\270\200.docx" differ diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\203\346\214\226\347\237\277\345\222\214\345\205\261\350\257\206.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\203\346\214\226\347\237\277\345\222\214\345\205\261\350\257\206.md" new file mode 100644 index 0000000..606157a --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\203\346\214\226\347\237\277\345\222\214\345\205\261\350\257\206.md" @@ -0,0 +1,338 @@ +# 挖矿和共识 + +
+ +## 一、挖矿和工作量证明算法 +
+前面基本把区块链的交易加密啥的都过了一次,现在过大家比较关心的挖矿和共识,其实挖矿和共识放在一起做为一章,是因为二者是不好分开的,挖矿的过程过程其实也是一个变相等待共识的过程。哪个先挖矿出来,生成最新的区块,就会得到奖励,包括创币奖励和交易费奖励。只不过创币奖励会越来越少,直到没有。 +
+挖矿并不是真正的去矿山开采,而是用电脑的CPU,GPU或者专门的芯片甚至由专门的芯片组成的机器群俗称矿池来做一种哈希运算,前面提到过,其实就是对区块头进行哈希算法,直到这个算法的结果满足设定的难度需求。 +
+在新的源码中,挖矿分成了两部分,即基本的CPU挖矿和使用独立的挖矿单元来挖矿,主要分成了以下两种方法 +
+1、直接挖矿: +
+目前基本没人挖矿了,因为基本挖不到,但是代码得有: +
+ +``` C++ + +struct CBlockTemplate +{ + CBlock block; + std::vector vTxFees; + std::vector vTxSigOpsCost; + std::vector vchCoinbaseCommitment; +}; +//利用上面的模板生成相关的区块,并填充数据-下面的函数为通用的产生新块的函数在miner.cpp 中。 +std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn, bool fMineWitnessTx) +{ + int64_t nTimeStart = GetTimeMicros(); + + resetBlock(); + + pblocktemplate.reset(new CBlockTemplate()); + + if(!pblocktemplate.get()) + return nullptr; + pblock = &pblocktemplate->block; // pointer for convenience + + // Add dummy coinbase tx as first transaction + pblock->vtx.emplace_back(); + pblocktemplate->vTxFees.push_back(-1); // updated at end + pblocktemplate->vTxSigOpsCost.push_back(-1); // updated at end + + LOCK2(cs_main, mempool.cs); + CBlockIndex* pindexPrev = chainActive.Tip(); + assert(pindexPrev != nullptr); + nHeight = pindexPrev->nHeight + 1; + + pblock->nVersion = ComputeBlockVersion(pindexPrev, chainparams.GetConsensus()); + // -regtest only: allow overriding block.nVersion with + // -blockversion=N to test forking scenarios + if (chainparams.MineBlocksOnDemand()) + pblock->nVersion = gArgs.GetArg("-blockversion", pblock->nVersion); + + pblock->nTime = GetAdjustedTime(); + const int64_t nMedianTimePast = pindexPrev->GetMedianTimePast(); + + nLockTimeCutoff = (STANDARD_LOCKTIME_VERIFY_FLAGS & LOCKTIME_MEDIAN_TIME_PAST) + ? nMedianTimePast + : pblock->GetBlockTime(); + + // Decide whether to include witness transactions + // This is only needed in case the witness softfork activation is reverted + // (which would require a very deep reorganization) or when + // -promiscuousmempoolflags is used. + // TODO: replace this with a call to main to assess validity of a mempool + // transaction (which in most cases can be a no-op). + //处理隔离见证 + fIncludeWitness = IsWitnessEnabled(pindexPrev, chainparams.GetConsensus()) && fMineWitnessTx; + + int nPackagesSelected = 0; + int nDescendantsUpdated = 0; + addPackageTxs(nPackagesSelected, nDescendantsUpdated); + + int64_t nTime1 = GetTimeMicros(); + + nLastBlockTx = nBlockTx; + nLastBlockWeight = nBlockWeight; + + // Create coinbase transaction. + CMutableTransaction coinbaseTx; + coinbaseTx.vin.resize(1); + coinbaseTx.vin[0].prevout.SetNull(); + coinbaseTx.vout.resize(1); + coinbaseTx.vout[0].scriptPubKey = scriptPubKeyIn; + //费用+减半机制得到的创币奖励 + coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus()); + coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; + pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); + pblocktemplate->vchCoinbaseCommitment = GenerateCoinbaseCommitment(*pblock, pindexPrev, chainparams.GetConsensus()); + pblocktemplate->vTxFees[0] = -nFees; + + LogPrintf("CreateNewBlock(): block weight: %u txs: %u fees: %ld sigops %d\n", GetBlockWeight(*pblock), nBlockTx, nFees, nBlockSigOpsCost); + + // Fill in header + pblock->hashPrevBlock = pindexPrev->GetBlockHash(); + UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev);//更新难度,这个函数里调用下文的难度计算函数 + pblock->nBits = GetNextWorkRequired(pindexPrev, pblock, chainparams.GetConsensus()); + pblock->nNonce = 0; + pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); + + CValidationState state; + if (!TestBlockValidity(state, chainparams, *pblock, pindexPrev, false, false)) { + throw std::runtime_error(strprintf("%s: TestBlockValidity failed: %s", __func__, FormatStateMessage(state))); + } + int64_t nTime2 = GetTimeMicros(); + + LogPrint(BCLog::BENCH, "CreateNewBlock() packages: %.2fms (%d packages, %d updated descendants), validity: %.2fms (total %.2fms)\n", 0.001 * (nTime1 - nTimeStart), nPackagesSelected, nDescendantsUpdated, 0.001 * (nTime2 - nTime1), 0.001 * (nTime2 - nTimeStart)); + + return std::move(pblocktemplate); +} + +``` +
+上面的函数会被两个地方调用,一个是generateBlocks,一个是getblocktemplate,这两个函数都在mining.cpp中。这就到了RPC的模块范围内了。大家应该明白这个应该是供外部调用了。 +
+getblocktemplate这个主要是提供给现在真正的挖矿的矿池和矿机用的,芯片上的软件通过不断的调用它,然后通过芯片硬件的HASH计算来得到结果并填充这个区块来产生真正的区块。 +
+重点说一下前者,generateBlocks,来分析一下挖矿的过程。 +
+ +``` C++ +UniValue generateBlocks(std::shared_ptr coinbaseScript, int nGenerate, uint64_t nMaxTries, bool keepScript) +{ + static const int nInnerLoopCount = 0x10000; + int nHeightEnd = 0; + int nHeight = 0; + + { // Don't keep cs_main locked + LOCK(cs_main); + nHeight = chainActive.Height(); + nHeightEnd = nHeight+nGenerate; + } + unsigned int nExtraNonce = 0; + UniValue blockHashes(UniValue::VARR); + + //循环来挖矿 + while (nHeight < nHeightEnd && !ShutdownRequested()) + { + //创建区块 + std::unique_ptr pblocktemplate(BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript)); + if (!pblocktemplate.get()) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); + CBlock \*pblock = &pblocktemplate->block; + { + LOCK(cs_main); + //处理Nonce + IncrementExtraNonce(pblock, chainActive.Tip(), nExtraNonce); + } + //真正的循环计算哈希并进行验证 + while (nMaxTries > 0 && pblock->nNonce < nInnerLoopCount && !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) { + ++pblock->nNonce; + --nMaxTries; + } + if (nMaxTries == 0) { + break; + } + if (pblock->nNonce == nInnerLoopCount) { + continue; + } + std::shared_ptr shared_pblock = std::make_shared(\*pblock); + //处理新块,验证并保存 + if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); + ++nHeight; + blockHashes.push_back(pblock->GetHash().GetHex()); + + //mark script as important because it was used at least for one coinbase output if the script came from the wallet + if (keepScript) + { + coinbaseScript->KeepScript(); + } + } + return blockHashes; +} +``` +
+pblock->GetHash()负责不断的对头进行哈希取值,然后进行CheckProofOfWork,不对的话nonce++,再来,直到条件终止。真正的挖矿,就在这行代码里的这个函数里。 +
+ +## 二、共识 +
+下来就开始共识的部分了: +
+ +``` c++ + +bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params) +{ + bool fNegative; + bool fOverflow; + arith_uint256 bnTarget; + + //bBits,是当前块中的难度值 + bnTarget.SetCompact(nBits, &fNegative, &fOverflow); + + // Check range 检测难度值是否合理 + if (fNegative || bnTarget == 0 || fOverflow || bnTarget > UintToArith256(params.powLimit)) + return false; + + // Check proof of work matches claimed amount + if (UintToArith256(hash) > bnTarget)//这里进行比较 + return false; + + return true; +} + +``` +
+params.powLimit是原始的难度,即下文提到的最小难度,bnTarget这个就是真正的计算难度。如果和新挖区块的难度比较,小则返回False,大则表明挖矿成功。但是这个难度不是一成不变的: +
+ + +``` C++ +//nFirstBlockTime即前2016个块的第一个块的时间戳 +unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params) +{ + if (params.fPowNoRetargeting) + return pindexLast->nBits; + + // Limit adjustment step计算生成这2016个块花费的时间 + int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime; + if (nActualTimespan < params.nPowTargetTimespan/4)//不小于3.5天 + nActualTimespan = params.nPowTargetTimespan/4; + if (nActualTimespan > params.nPowTargetTimespan*4)//不大于56天 + nActualTimespan = params.nPowTargetTimespan*4; + + // Retarget + const arith_uint256 bnPowLimit = UintToArith256(params.powLimit); + arith_uint256 bnNew; + bnNew.SetCompact(pindexLast->nBits); + //计算前2016个块的难度总和,即单个块的难度*总时间 + bnNew \*= nActualTimespan; + //计算新的难度,即2016个块的难度总和/14天的秒数 + bnNew /= params.nPowTargetTimespan; + + if (bnNew > bnPowLimit) + bnNew = bnPowLimit; + + return bnNew.GetCompact(); +} +``` +
+比特币计算新的难度保存在新块的nBits中,这个上面提到了。说一下计算的步骤 +
+ +1、找到前2016个块的第一个块。 +
+ +2、计算生成这2016个块花费的时间,即最后一个块的时间与第一个块的时间差。时间差在3.5~56天之间。 +
+ +3、计算前2016块的难度总和,即单个块的难度*时间 +
+ +4、计算新难度,即难度总和/14天的秒数,得到每秒的难度值。 +
+ +5、新的难度不能大于参数定义的最小难度。 +
+在测试网络和主网略有不同。 +
+原来比特币系统提供了一个函数计算一段时间内的最小难度,ComputeMinWork, 新版本应该是没有了,但还是有一个参数设置了最小难度,即上文提到的params.powLimit,它的作用如下: +
+ +1、在测试网络中如果时间区间大于28天,则最小难度为它,即参数定义的最小难度。 +
+ +2、时间以56天递减,难度4倍递增,循环计算,计算出新难度。 +
+ +3、新难度不能大于这个最小难度。 +
+ +## 三、组装到链 +
+挖矿和共识完成后,就是挂到主链上,保存,更新索引,各种验证,然后上网传播区块了,这个在交易和网络中都有涉及,这里就不再细说。只说明一下流程: + +``` c++ +UniValue generateBlocks(std::shared_ptr coinbaseScript, int nGenerate, uint64_t nMaxTries, bool keepScript) +{ +...... + if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); +...... +} +bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr pblock, bool fForceProcessing, bool *fNewBlock) +{ +...... + { + + if (ret) { + // Store to disk + ret = g_chainstate.AcceptBlock(pblock, state, chainparams, &pindex, fForceProcessing, nullptr, fNewBlock); + } + if (!ret) { + //如果失败就处理一下异常 + GetMainSignals().BlockChecked(\*pblock, state); + } + } +} +...... + +bool CChainState::AcceptBlock(const std::shared_ptr& pblock, CValidationState& state, const CChainParams& chainparams, CBlockIndex** ppindex, bool fRequested, const CDiskBlockPos* dbp, bool* fNewBlock) +{ +...... + + // Header is valid/has work, merkle tree and segwit merkle tree are good...RELAY NOW + // (but if it does not build on our best tip, let the SendMessages loop relay it) + if (!IsInitialBlockDownload() && chainActive.Tip() == pindex->pprev) + GetMainSignals().NewPoWValidBlock(pindex, pblock); +...... +} + +/** + * Maintain state about the best-seen block and fast-announce a compact block + * to compatible peers. + */ + //从BIP152开始使用紧凑块,提升传输速度,减少传送亘。 +void PeerLogicValidation::NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr& pblock) { + ...... + connman->PushMessage(pnode, msgMaker.Make(NetMsgType::CMPCTBLOCK, *pcmpctblock)); + state.pindexBestHeaderSent = pindex; +...... +} + +``` +
+从上面可以看到,在创建出一个新块后,直接调用ProcessNewBlock来处理它(这里把挂到主链上的代码忽略掉了,主要是为了突出网络发送的流程)。在AcceptBlock函数中,调用GetMainSignals().NewPoWValidBlock发送紧凑块,提升性能。而在发送中看到前面熟悉的PushMessage,这个就直接从网络广播出去了。 +
+ +## 四、分叉 + +
+分叉有软硬之分,软分叉一般是软件升级造成的版本不同出现的,一般来,大家把版本升级后就会自动融合。硬分叉一般是共识机制出现了重大变化,造成区块分叉。 +
+
diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\211\351\222\261\345\214\205\345\222\214\344\272\244\346\230\223.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\211\351\222\261\345\214\205\345\222\214\344\272\244\346\230\223.md" new file mode 100644 index 0000000..4936722 --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\270\211\351\222\261\345\214\205\345\222\214\344\272\244\346\230\223.md" @@ -0,0 +1,1081 @@ +# 精通比特币三钱包和交易 +*** +
+ +# 一、钱包的种类 + +
+ 1.非确定性钱包 + 早期的钱包基本都是由随机种子随机发生,所以每次都会产生完全没关系的互相独立的私钥。 +
+ 2.确定性钱包 + 确定性,或者“种子”钱包包含通过使用单项离散函数而可从公共的种子生成的私钥。 +
主要是指分层确定性钱包(BIP-32/BIP-44),它由助词词(BIP-39)生成随机种子来产生一系列的密钥。使用助词词的优势就在于可以把钱包固化到硬件中。这样就不容易因为遗忘等原因千万密钥的丢失。当然,在某种程度上也提高了安全性。 +
+ +# 二、创建钱包 + +
**助词的产生** +
在github上可以看到有八种语言(英、日、韩、西班牙、中繁、简、法和意大利)基本词, **助记词经常与“脑钱包”混淆** 。 他们不一样。主要区别在于脑钱包由用户选择的单词组成,而助记符是由钱包随机创建的,并呈现给用户。 +
中文的开头几个:的 一 是 在 不 了...... +
英文的开头几个:abandon ability able about above + ![助词词的网站](img/mnemonic.png) +
助词词和种子产生的过程: +
1)创建一个128到256位的随机序列(熵)。 +
2)提出SHA256哈希前几位(熵长/ 32),就可以创造一个随机序列的校验和。 +
3)将校验和添加到随机序列的末尾。 +
4)将序列划分为包含11位的不同部分。 +
5)将每个包含11位部分的值与一个已经预先定义2048个单词的字典做对应。 +
6)生成的有顺序的单词组就是助记码。 +
7)PBKDF2密钥延伸函数的第一个参数是从步骤6生成的助记符。 +
8)PBKDF2密钥延伸函数的第二个参数是盐。 由字符串常数“助记词”与可选的用户提供的密码字符串连接组成。 +
9)PBKDF2使用HMAC-SHA512算法,使用2048次哈希来延伸助记符和盐参数,产生一个512位的值作为其最终输出。 这个512位的值就是种子。 +
**可选密码短语** +
觉得类似于密码,但是又比密码功能多。 +
BIP-39标准允许在推导种子时使用可选的密码短语。 如果没有使用密码短语,助记词是用由常量字符串“助记词”构成的盐进行延伸,从任何给定的助记词产生一个特定的512位种子。 如果使用密码短语,密钥延伸函数使用同样的助记词也会产生不同的种子。事实上,给予一个单一的助记词,每一个可能的密码短语都会导致不同的种子。 基本上没有“错误”的密码短语, 所有密码短语都是有效的,它们都会导致不同的种子,形成一大批可能未初始化的钱包。这批钱包非常之大(2^512),使用暴力破解或随机猜测基本不可能。 +
不过这个容易引起不可逆转的密钥丢失,比如短语知晓人突然因为各种情况失忆或者去世。那么钱包中的币就没了。 +
**从种子创建钱包** : +
以libbitcoin为例,它实现了两种方法即书讲的早期的electrum方法和BIP-39事实上的标准方法,只介绍后者,前者大家去看书 +
+//dictionary:助记词的字典 +``` C++ +word_list create_mnemonic(data_slice entropy, const dictionary &lexicon) + { + if ((entropy.size() % mnemonic_seed_multiple) != 0) + return {}; + + const size_t entropy_bits = (entropy.size() * byte_bits); + const size_t check_bits = (entropy_bits / entropy_bit_divisor); + const size_t total_bits = (entropy_bits + check_bits); + const size_t word_count = (total_bits / bits_per_word); + + BITCOIN_ASSERT((total_bits % bits_per_word) == 0); + BITCOIN_ASSERT((word_count % mnemonic_word_multiple) == 0); + + const auto data = build_chunk({entropy, sha256_hash(entropy)}); + + size_t bit = 0; + word_list words; + + for (size_t word = 0; word < word_count; word++) + { + size_t position = 0; + for (size_t loop = 0; loop < bits_per_word; loop++) + { + bit = (word * bits_per_word + loop); + position <<= 1; + + const auto byte = bit / byte_bits; + + if ((data[byte] & bip39_shift(bit)) > 0) + position++; + } + + BITCOIN_ASSERT(position < dictionary_size); + words.push_back(lexicon[position]); + } + + BITCOIN_ASSERT(words.size() == ((bit + 1) / bits_per_word)); + return words; + } +``` +
重点看一下如何生成种子: + +``` C++ +long_hash decode_mnemonic(const word_list& mnemonic) + { + const auto sentence = join(mnemonic); + const std::string salt(passphrase_prefix);//加盐 + //PBKDF2哈希算法 + return pkcs5_pbkdf2_hmac_sha512(to_chunk(sentence), to_chunk(salt), + hmac_iterations); + } + +long_hash pkcs5_pbkdf2_hmac_sha512(data_slice passphrase, + data_slice salt, size_t iterations) + { + long_hash hash; + const auto result = pkcs5_pbkdf2(passphrase.data(), passphrase.size(), + salt.data(), salt.size(), hash.data(), hash.size(), iterations); + + if (result != 0) + throw std::bad_alloc(); + + return hash; + } +void HMACSHA512(const uint8_t* input, size_t length, const uint8_t* key, + size_t key_length, uint8_t digest[HMACSHA512_DIGEST_LENGTH]) +{ + HMACSHA512CTX context; + HMACSHA512Init(&context, key, key_length); + HMACSHA512Update(&context, input, length); + HMACSHA512Final(&context, digest); +} +``` +
+内部会调用相关的哈希算法,HMAC-SHA512算法,使用2048次哈希来延伸助记符和盐参数,产生一个512位的值作为其最终输出。 这个512位的值就是种子。 +算法的具体流程大家看相关的代码。在hmac_sha512.c sha512.c pkcs5_pbkdf2.c三个主要的文件中。 +
**从种子创建钱包** +
有了种子就可以创建钱包相关的私钥和公钥了。如下图 +
+ + ![种子生成钱包](img/seedtokey.png) +
+ +``` C++ +hd_private hd_private::from_seed(data_slice seed, uint64_t prefixes) +{ + // This is a magic constant from BIP32. + static const data_chunk magic(to_chunk("Bitcoin seed")); + + const auto intermediate = split(hmac_sha512_hash(seed, magic)); + + // The key is invalid if parse256(IL) >= n or 0://判断左侧的256 + if (!verify(intermediate.left)) + return {}; + + const auto master = hd_lineage + { + prefixes, + 0x00, + 0x00000000, + 0x00000000 + }; + //返回两部分,即左侧为私钥,右侧为CHAIN,各256 + return hd_private(intermediate.left, intermediate.right, master); + } +``` +//导出私钥 + +``` C++ +hd_private hd_private::derive_private(uint32_t index) const +{ + constexpr uint8_t depth = 0; + + const auto data = (index >= hd_first_hardened_key) ? + splice(to_array(depth), secret_, to_big_endian(index)) : + splice(point_, to_big_endian(index)); + + const auto intermediate = split(hmac_sha512_hash(data, chain_)); + + // The child key ki is (parse256(IL) + kpar) mod n: + auto child = secret_; + if (!ec_add(child, intermediate.left)) + return {}; + + if (lineage_.depth == max_uint8) + return {}; + + const hd_lineage lineage + { + lineage_.prefixes, + static_cast(lineage_.depth + 1), + fingerprint(), + index + }; + + return hd_private(child, intermediate.right, lineage); +} + +//产生公钥 + +hd_public hd_public::from_secret(const ec_secret& secret, + const hd_chain_code& chain_code, const hd_lineage& lineage) +{ + ec_compressed point; + return secret_to_public(point, secret) ? + hd_public(point, chain_code, lineage) : hd_public{}; +} + //导出公钥 + hd_public hd_public::derive_public(uint32_t index) const +{ + // + if (index >= hd_first_hardened_key) + return {}; + + //static BC_CONSTEXPR size_t ec_compressed_size = 33; + //typedef byte_array ec_compressed; + //point_是一个压缩格式的公钥看上面的定义33个BYTE,66个十六进制数 + const auto data = splice(point_, to_big_endian(index)); + const auto intermediate = split(hmac_sha512_hash(data, chain_)); + + // The returned child key Ki is point(parse256(IL)) + Kpar. + auto combined = point_; + //调用哈希算法 + if (!ec_add(combined, intermediate.left)) + return {}; + + if (lineage_.depth == max_uint8) + return {}; + + const hd_lineage lineage + { + lineage_.prefixes, + static_cast(lineage_.depth + 1), + fingerprint(), + index + }; + + return hd_public(combined, intermediate.right, lineage); +} + +//回到了类似比特币源码中创建公钥的过程 + +template +bool ec_add(const secp256k1_context* context, byte_array& in_out, + const ec_secret& secret) +{ + secp256k1_pubkey pubkey; + return parse(context, pubkey, in_out) && + secp256k1_ec_pubkey_tweak_add(context, &pubkey, secret.data()) == 1 && + serialize(context, in_out, pubkey); +} +``` +
+ +# 三、密钥的衍生 + +
强化衍生和正常衍生:一个是从母公钥衍生子链码,一个是从母私钥衍生子链码。为了区别二者,产生的索引号区分了范围: +
前者是0\~2\^31-1,后者是2\^31\~3\^32-1 +
由主私钥衍生出的私钥起始以“m”打头。由主公钥衍生的公钥起始以“M“打头。因此,母密钥生成的第一个子私钥是m/0。第一个公钥是M/0。第一个子密钥的子密钥就是m/0/1,以此类推。 +
**钱包树状结构的导航** +
钱包树太灵活了,不容易控制。所以BIP-43,44提出了多帐户结构。 +
BIP-43提出使用第一个强化子索引作为特殊的标识符表示树状结构的“purpose”。基于BIP-43,HD钱包应该使用且只用第一层级的树的分支,而且有索引号码去识别结构并且有命名空间来定义剩余的树的目的地。举个例子,HD钱包只使用分支m/i'/是 为了表明那个被索引号“i”定义的特殊为目地。 + +在BIP-43标准下,为了延长的那个特殊规范,BIP-44提议了多账户结构作为“purpose”。所有遵循BIP-44的HD钱包依据只使用树的第一个分支的要求而被定义:m/44'/。 BIP-44指定了包含5个预定义树状层级的结构: +
+m / purpose' / coin_type' / account' / change / address_index +
+第一层的purpose总是被设定为44'。 +
+第二层的“coin_type”特指币种并且允许多元货币HD钱包中的货币在第二个层级下有自己的亚树状结构。目前有三种货币被定义:Bitcoin is m/44'/0'、Bitcoin Testnet is m/44'/1',以及 Litecoin is m/44'/2'。 +
+树的第三层级是“account”,这可以允许使用者为了会计或者组织目的,而去再细分他们的钱包到独立的逻辑性亚账户。 举个例子,一个HD钱包可能包含两个比特币“账户”:m/44'/0'/0' 和 m/44'/0'/1'。每个账户都是它自己亚树的根。 +
+第四层级就是“change”。每一个HD钱包有两个亚树,一个是用来接收地址一个是用来创造找零地址。注意无论先前的层级是否使用强化衍生,这一层级使用的都是常规衍生。这是为了允许这一层级的树可以在不安全环境下,输出扩展公钥。 +
+被HD钱包衍生的可用的地址是第四层级的子级,就是第五层级的树的“address_index”。 +
+ +# 四、交易 + +
还是得先上一章交易图 + ![交易图](img/transfer.png) +
来查看一下比特币的发送代码: +
sendcoinsdialog.cpp:这个是钱包的发送界面类 +``` c++ +void SendCoinsDialog::on_sendButton_clicked() + { + if(!model || !model->getOptionsModel()) + return; +...... + + // prepare transaction for getting txFee earlier + WalletModelTransaction currentTransaction(recipients); + WalletModel::SendCoinsReturn prepareStatus; + //prepareTransaction中会创建一个新交易,并设置交易的手续费 + if (model->getOptionsModel()->getCoinControlFeatures()) // coin control enabled + prepareStatus = model->prepareTransaction(currentTransaction, CoinControlDialog::coinControl); + else + prepareStatus = model->prepareTransaction(currentTransaction); + + // process prepareStatus and on error generate message shown to user + processSendCoinsReturn(prepareStatus, + BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee())); + + if(prepareStatus.status != WalletModel::OK) { + fNewRecipientAllowed = true; + return; + } + + CAmount txFee = currentTransaction.getTransactionFee(); +...... + + // now send the prepared transaction + WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction); + // process sendStatus and on error generate message shown to user + processSendCoinsReturn(sendStatus); +...... + } +``` +
交易的创建 +``` C++ +WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction \&transaction, const CCoinControl\*coinControl) +{ +...... +// Pre-check input data for validity +Q_FOREACH(const SendCoinsRecipient \& rcp, recipients) +{ + if (rcp.fSubtractFeeFromAmount) + fSubtractFeeFromAmount = true; + + if (rcp.paymentRequest.IsInitialized()) + { + // PaymentRequest... + CAmount subtotal = 0; + const payments::PaymentDetails& details = rcp.paymentRequest.getDetails(); + for (int i = 0; i < details.outputs_size(); i++) + { + const payments::Output\& out = details.outputs(i); + if (out.amount() <= 0) continue; + subtotal += out.amount(); + const unsigned char\* scriptStr = (const unsigned char \*)out.script().data(); + CScript scriptPubKey(scriptStr, scriptStr+out.script().size()); + CAmount nAmount = out.amount(); + CRecipient recipient = {scriptPubKey, nAmount, rcp.fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + } + if (subtotal <= 0) + { + return InvalidAmount; + } + total += subtotal; + } + else + { // User-entered bitcoin address amount: + if(\!validateAddress(rcp.address)) + { + return InvalidAddress; + } + if(rcp.amount <= 0) + { + return InvalidAmount; + } + setAddress.insert(rcp.address); + ++nAddresses; + + CScript scriptPubKey = GetScriptForDestination(CBitcoinAddress(rcp.address.toStdString()).Get()); + CRecipient recipient = {scriptPubKey, rcp.amount, rcp.fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + + total += rcp.amount; + } + } +...... + +{ + LOCK2(cs_main, wallet->cs_wallet); + + transaction.newPossibleKeyChange(wallet); + + CAmount nFeeRequired = 0; + int nChangePosRet = -1; + std::string strFailReason; + + CWalletTx \*newTx = transaction.getTransaction(); + CReserveKey \*keyChange = transaction.getPossibleKeyChange(); + //创建交易 + bool fCreated = wallet->CreateTransaction(vecSend, *newTx, *keyChange, nFeeRequired, nChangePosRet, strFailReason, coinControl); + //设置交易费 + transaction.setTransactionFee(nFeeRequired); + if (fSubtractFeeFromAmount && fCreated) + transaction.reassignAmounts(nChangePosRet); +....... + } +} + +``` + +
继续看sendCoins这个函数: + +``` C++ +WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction &transaction) +{ + QByteArray transaction_array; // store serialized transaction + { + LOCK2(cs_main, wallet->cs_wallet); + CWalletTx \*newTx = transaction.getTransaction(); + + Q_FOREACH(const SendCoinsRecipient &rcp, transaction.getRecipients()) + { +.... + rcp.paymentRequest.SerializeToString(&value); + newTx->vOrderForm.push_back(make_pair(key, value)); + } + else if (!rcp.message.isEmpty()) // Message from normal bitcoin:URI (bitcoin:123...\?message=example) + newTx->vOrderForm.push_back(make_pair("Message", rcp.message.toStdString())); + } + + CReserveKey \*keyChange = transaction.getPossibleKeyChange(); + if(!wallet->CommitTransaction(\*newTx, \*keyChange)) + return TransactionCommitFailed; + + CTransaction\* t = (CTransaction*)newTx; + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << \*t; + transaction_array.append(&(ssTx[0]), ssTx.size()); + } + + // Add addresses / update labels that we've sent to to the address book, + // and emit coinsSent signal for each recipient + Q_FOREACH(const SendCoinsRecipient &rcp, transaction.getRecipients()) + { + // Don't touch the address book when we have a payment request +..... + Q_EMIT coinsSent(wallet, rcp, transaction_array); + } + checkBalanceChanged(); // update balance immediately, otherwise there could be a short noticeable delay until pollBalanceChanged hits + + return SendCoinsReturn(OK); +} +``` + +``` C++ +bool CWallet::CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey) +{ + { + LOCK2(cs_main, cs_wallet); + LogPrintf("CommitTransaction:\n%s", wtxNew.ToString()); + { + // This is only to keep the database open to defeat the auto-flush for the + // duration of this scope. This is the only place where this optimization + // maybe makes sense; please don't do it anywhere else. + CWalletDB* pwalletdb = fFileBacked ? new CWalletDB(strWalletFile,"r+") : NULL; + + // Take key pair from key pool so it won't be used again + reservekey.KeepKey(); + + // Add tx to wallet, because if it has change it's also ours, + // otherwise just for transaction history. + AddToWallet(wtxNew, false, pwalletdb); +..... + } + + // Track how many getdata requests our transaction gets + mapRequestCount[wtxNew.GetHash()] = 0; + + if (fBroadcastTransactions) + { + // Broadcast + if (!wtxNew.AcceptToMemoryPool(false)) + {... + } + wtxNew.RelayWalletTransaction(); + } + } + return true; +} +``` + +
RelayWalletTransaction调用RelayTransaction + +``` C++ +void RelayTransaction(const CTransaction& tx) +{ + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss.reserve(10000); + ss << tx; + RelayTransaction(tx, ss); +} +``` +``` C++ +void RelayTransaction(const CTransaction& tx, const CDataStream& ss) +{ + //初始化一个INV,供下面发送 + CInv inv(MSG_TX, tx.GetHash()); + { + LOCK(cs_mapRelay); + // Expire old relay messages + while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime()) + { + mapRelay.erase(vRelayExpiration.front().second); + vRelayExpiration.pop_front(); + } + + // Save original serialized message so newer versions are preserved + mapRelay.insert(std::make_pair(inv, ss)); + vRelayExpiration.push_back(std::make_pair(GetTime() + 15 * 60, inv)); + } + LOCK(cs_vNodes); + BOOST_FOREACH(CNode* pnode, vNodes) + { + if(!pnode->fRelayTxes) + continue; + LOCK(pnode->cs_filter); + if (pnode->pfilter) + { + if (pnode->pfilter->IsRelevantAndUpdate(tx)) + pnode->PushInventory(inv); + } else + pnode->PushInventory(inv);//到这里基本发送的INV数据准备完成。 + } +} +``` +``` C++ +void PushInventory(const CInv& inv) +{ + { + LOCK(cs_inventory); + if (!setInventoryKnown.count(inv)) + vInventoryToSend.push_back(inv);//存储到相关的VECtor + } +} +``` +下面是驱动本节点数据向外分发交易数据的部分,即把vInventoryToSend中的数据发送P2P网络: +
在前面的启动分析过程中知道,在aapinit2中会调用StartNode--ThreadMessageHandler:在其中通过BOOST的信号连接: +
g_signals.SendMessages(pnode, pnode == pnodeTrickle || pnode->fWhitelisted);**循环调用SendMessages** +
+ +``` C++ +bool SendMessages(CNode* pto, bool fSendTrickle) + { + ...... + // + // Message: inventory + // + vector vInv; + vector vInvWait; + { + LOCK(pto->cs_inventory); + vInv.reserve(pto->vInventoryToSend.size()); + vInvWait.reserve(pto->vInventoryToSend.size()); + BOOST_FOREACH(const CInv& inv, pto->vInventoryToSend) + { + if (pto->setInventoryKnown.count(inv)) + continue; + + // trickle out tx inv to protect privacy + if (inv.type == MSG_TX && !fSendTrickle) + { + // 1/4 of tx invs blast to all immediately + static uint256 hashSalt; + if (hashSalt.IsNull()) + hashSalt = GetRandHash(); + uint256 hashRand = ArithToUint256(UintToArith256(inv.hash) ^ UintToArith256(hashSalt)); + hashRand = Hash(BEGIN(hashRand), END(hashRand)); + bool fTrickleWait = ((UintToArith256(hashRand) & 3) != 0); + + if (fTrickleWait) + { + vInvWait.push_back(inv); + continue; + } + } + // returns true if wasn't already contained in the set + if (pto->setInventoryKnown.insert(inv).second) + { + vInv.push_back(inv); + if (vInv.size() >= 1000) + { + pto->PushMessage("inv", vInv); + vInv.clear(); + } + } + } + pto->vInventoryToSend = vInvWait; + } + if (!vInv.empty()) + pto->PushMessage("inv", vInv); + ...... + //同步区块 + // Message: getdata (blocks) + //nBlocksInFlight:已发出请求,但尚未接收到的区块数量 + vector vGetData; + if (!pto->fDisconnect && !pto->fClient && (fFetch || !IsInitialBlockDownload()) && state.nBlocksInFlight < MAX_BLOCKS_IN_TRANSIT_PER_PEER) { + vector vToDownload; + NodeId staller = -1; + FindNextBlocksToDownload(pto->GetId(), MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.nBlocksInFlight, vToDownload, staller); + BOOST_FOREACH(CBlockIndex \*pindex, vToDownload) { + vGetData.push_back(CInv(MSG_BLOCK, pindex->GetBlockHash())); + MarkBlockAsInFlight(pto->GetId(), pindex->GetBlockHash(), consensusParams, pindex); + ..... + } + // + // Message: getdata (non-blocks):其它数据 + // + while (!pto->fDisconnect && !pto->mapAskFor.empty() && (\*pto->mapAskFor.begin()).first <= nNow) + { + const CInv& inv = (\*pto->mapAskFor.begin()).second; + if (!AlreadyHave(inv)) + { + if (fDebug) + LogPrint("net", "Requesting %s peer=%d\n", inv.ToString(), pto->id); + vGetData.push_back(inv); + if (vGetData.size() >= 1000) + { + //发送其它数据 + pto->PushMessage("getdata", vGetData); + vGetData.clear(); + } + } + pto->mapAskFor.erase(pto->mapAskFor.begin()); + } + //发送区块数据 + if (!vGetData.empty()) + pto->PushMessage("getdata", vGetData); + } + return true; + } +``` +
来看一下那个PushMessage: +
+``` C++ +template + void PushMessage(const char* pszCommand, const T1& a1) + { + try + { + BeginMessage(pszCommand); + ssSend << a1; + EndMessage(); + } + catch (...) + { + AbortMessage(); + throw; + } + } +``` +重点在EndMessage中: +``` C++ +void CNode::EndMessage() UNLOCK_FUNCTION(cs_vSend) +{ +...... + std::deque::iterator it = vSendMsg.insert(vSendMsg.end(), CSerializeData()); + ssSend.GetAndClear(\*it); + nSendSize += (\*it).size(); + + // If write queue empty, attempt "optimistic write" + if (it == vSendMsg.begin()) + SocketSendData(this);//通过这个函数用SOCKET发送到了相应的节点上。 + + LEAVE_CRITICAL_SECTION(cs_vSend); +} +``` +
下面分析一下接收交易,首先看一下数据的接收流程: +``` C++ + +//说明:数据的接收来自于ThreadSocketHandler中的Recv,采用的Select模型。 +//ThreadMessageHandler中通过g_signals.ProcessMessages(pnode)来激发下面的循环 +bool ProcessMessages(CNode* pfrom) +{ +...... + //消息头的格式,下面省略了处理这些的过程,只关心数据 + // Message format + // (4) message start + // (12) command + // (4) size + // (4) checksum + // (x) data + // + bool fOk = true; + + if (!pfrom->vRecvGetData.empty()) + ProcessGetData(pfrom);//接收数据 + + // this maintains the order of responses + if (!pfrom->vRecvGetData.empty()) return fOk; + + std::deque::iterator it = pfrom->vRecvMsg.begin(); + while (!pfrom->fDisconnect && it != pfrom->vRecvMsg.end()) { +...... + + // Process message + bool fRet = false; + try + { + //发送消息,继续处理 + fRet = ProcessMessage(pfrom, strCommand, vRecv, msg.nTime); + boost::this_thread::interruption_point(); + } + catch (const std::ios_base::failure& e) + { + //发送错误消息 + pfrom->PushMessage("reject", strCommand, REJECT_MALFORMED, string("error parsing message")); +...... + } + break; + } + +..... +} +``` +
然后里面有两个重要的函数:ProcessGetData 和ProcessMessage (重载) +
+ ``` C++ + void static ProcessGetData(CNode* pfrom) + { + ...... + + while (it != pfrom->vRecvGetData.end()) { + // Don't bother if send buffer is too full to respond anyway + if (pfrom->nSendSize >= SendBufferSize()) + break; + + const CInv &inv = \*it; + { + ...... + + if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK) + { + //块消息略过 + ...... + } + else if (inv.IsKnownType()) + { + // Send stream from relay memory + bool pushed = false; + { + LOCK(cs_mapRelay); + //mapRelay存储着原始的INV,用来对比和更新 + map::iterator mi = mapRelay.find(inv); + if (mi != mapRelay.end()) { + pfrom->PushMessage(inv.GetCommand(), (*mi).second); + pushed = true; + } + } + //交易消息 + if (!pushed && inv.type == MSG_TX) { + CTransaction tx; + if (mempool.lookup(inv.hash, tx)) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss.reserve(1000); + ss << tx; + pfrom->PushMessage("tx", ss); + pushed = true; + } + } + + //都没有找到保存 + if (!pushed) { + vNotFound.push_back(inv); + } + } + + // Track requests for our stuff.发送消息,处理保存的INV + GetMainSignals().Inventory(inv.hash); + + if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK) + break; + } + } + //继续转发 + pfrom->PushMessage("notfound", vNotFound); + ...... + } + + void Inventory(const uint256 &hash) + { + { + LOCK(cs_wallet); + std::map::iterator mi = mapRequestCount.find(hash); + if (mi != mapRequestCount.end()) + (\*mi).second++; + } + } + ``` +
分发处理各类消息,这里删除其它无关: + ``` C++ + bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, int64_t nTimeReceived) + { + ...... + if (strCommand == "version") + { + ...... + } + else if (strCommand == "inv") + { + vector vInv; + vRecv >> vInv; + if (vInv.size() > MAX_INV_SZ) + { + Misbehaving(pfrom->GetId(), 20); + return error("message inv size() = %u", vInv.size()); + } + + LOCK(cs_main); + + std::vector vToFetch; + + for (unsigned int nInv = 0; nInv < vInv.size(); nInv++) + { + const CInv &inv = vInv[nInv]; + + boost::this_thread::interruption_point(); + pfrom->AddInventoryKnown(inv); + + bool fAlreadyHave = AlreadyHave(inv); + LogPrint("net", "got inv: %s %s peer=%d\n", inv.ToString(), fAlreadyHave ? "have" : "new", pfrom->id); + + if (!fAlreadyHave && !fImporting && !fReindex && inv.type != MSG_BLOCK) + pfrom->AskFor(inv); +...... + + // Track requests for our stuff + GetMainSignals().Inventory(inv.hash); + + if (pfrom->nSendSize > (SendBufferSize() * 2)) { + Misbehaving(pfrom->GetId(), 50); + return error("send buffer size() = %u", pfrom->nSendSize); + } + } + + if (!vToFetch.empty()) + pfrom->PushMessage("getdata", vToFetch); + } + ...... + else if (strCommand == "tx") + { + vector vWorkQueue; + vector vEraseQueue; + CTransaction tx; + vRecv >> tx; + + CInv inv(MSG_TX, tx.GetHash()); + pfrom->AddInventoryKnown(inv); + + LOCK(cs_main); + + bool fMissingInputs = false; + CValidationState state; + + mapAlreadyAskedFor.erase(inv); + + // Check for recently rejected (and do other quick existence checks) + if (AlreadyHave(inv)) + return true; + + if (AcceptToMemoryPool(mempool, state, tx, true, &fMissingInputs)) + { + mempool.check(pcoinsTip); + RelayTransaction(tx);//保存以供继续发送 + vWorkQueue.push_back(inv.hash); + + LogPrint("mempool", "AcceptToMemoryPool: peer=%d %s: accepted %s (poolsz %u)\n", + pfrom->id, pfrom->cleanSubVer, + tx.GetHash().ToString(), + mempool.mapTx.size()); + + // Recursively process any orphan transactions that depended on this one + set setMisbehaving; + for (unsigned int i = 0; i < vWorkQueue.size(); i++) + { + map >::iterator itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue[i]); + if (itByPrev == mapOrphanTransactionsByPrev.end()) + continue; + for (set::iterator mi = itByPrev->second.begin(); + mi != itByPrev->second.end(); + ++mi) + { + const uint256& orphanHash = \*mi; + const CTransaction& orphanTx = mapOrphanTransactions[orphanHash].tx; + NodeId fromPeer = mapOrphanTransactions[orphanHash].fromPeer; + bool fMissingInputs2 = false; + // Use a dummy CValidationState so someone can't setup nodes to counter-DoS based on orphan + // resolution (that is, feeding people an invalid transaction based on LegitTxX in order to get anyone relaying LegitTxX banned) + CValidationState stateDummy; + + + if (setMisbehaving.count(fromPeer)) + continue; + if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) + { + LogPrint("mempool", " accepted orphan tx %s\n", orphanHash.ToString()); + RelayTransaction(orphanTx); + vWorkQueue.push_back(orphanHash); + vEraseQueue.push_back(orphanHash); + } + else if (!fMissingInputs2) + { + int nDos = 0; + if (stateDummy.IsInvalid(nDos) && nDos > 0) + { + // Punish peer that gave us an invalid orphan tx + Misbehaving(fromPeer, nDos); + setMisbehaving.insert(fromPeer); + LogPrint("mempool", " invalid orphan tx %s\n", orphanHash.ToString()); + } + // Has inputs but not accepted to mempool + // Probably non-standard or insufficient fee/priority + LogPrint("mempool", " removed orphan tx %s\n", orphanHash.ToString()); + vEraseQueue.push_back(orphanHash); + assert(recentRejects); + recentRejects->insert(orphanHash); + } + mempool.check(pcoinsTip); + } + } + + BOOST_FOREACH(uint256 hash, vEraseQueue) + EraseOrphanTx(hash); + } + + } + ...... + } + + ``` + +
交易的代码比较重要,基本没删除,里面的注释也比较多 + ``` C++ + bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree, + bool* pfMissingInputs, bool fRejectAbsurdFee) + { + ..... + + // Coinbase is only valid in a block, not as a loose transaction + if (tx.IsCoinBase()) + return state.DoS(100, error("AcceptToMemoryPool: coinbase as individual tx"), + REJECT_INVALID, "coinbase"); + + ..... + + // is it already in the memory pool? + uint256 hash = tx.GetHash(); + if (pool.exists(hash)) + return false; + + // Check for conflicts with in-memory transactions + { + LOCK(pool.cs); // protect pool.mapNextTx + for (unsigned int i = 0; i < tx.vin.size(); i++) + { + COutPoint outpoint = tx.vin[i].prevout; + if (pool.mapNextTx.count(outpoint)) + { + // Disable replacement feature for now + return false; + } + } + } + + { + CCoinsView dummy; + CCoinsViewCache view(&dummy); + + CAmount nValueIn = 0; + { + LOCK(pool.cs); + CCoinsViewMemPool viewMemPool(pcoinsTip, pool); + view.SetBackend(viewMemPool); + + // do we already have it? + if (view.HaveCoins(hash)) + return false; + + // do all inputs exist? + // Note that this does not check for the presence of actual outputs (see the next check for that), + // and only helps with filling in pfMissingInputs (to determine missing vs spent). + BOOST_FOREACH(const CTxIn txin, tx.vin) { + if (!view.HaveCoins(txin.prevout.hash)) { + if (pfMissingInputs) + \*pfMissingInputs = true; + return false; + } + } + + // are the actual inputs available? + if (!view.HaveInputs(tx)) + return state.Invalid(error("AcceptToMemoryPool: inputs already spent"), + REJECT_DUPLICATE, "bad-txns-inputs-spent"); + + // Bring the best block into scope + view.GetBestBlock(); + + nValueIn = view.GetValueIn(tx); + + // we have all inputs cached now, so switch back to dummy, so we don't need to keep lock on mempool + view.SetBackend(dummy); + } + + // Check for non-standard pay-to-script-hash in inputs + if (Params().RequireStandard() && !AreInputsStandard(tx, view)) + return error("AcceptToMemoryPool: nonstandard transaction input"); + + // Check that the transaction doesn't have an excessive number of + // sigops, making it impossible to mine. Since the coinbase transaction + // itself can contain sigops MAX_STANDARD_TX_SIGOPS is less than + // MAX_BLOCK_SIGOPS; we still consider this an invalid rather than + // merely non-standard transaction. + unsigned int nSigOps = GetLegacySigOpCount(tx); + nSigOps += GetP2SHSigOpCount(tx, view); + if (nSigOps > MAX_STANDARD_TX_SIGOPS) + return state.DoS(0, + error("AcceptToMemoryPool: too many sigops %s, %d > %d", + hash.ToString(), nSigOps, MAX_STANDARD_TX_SIGOPS), + REJECT_NONSTANDARD, "bad-txns-too-many-sigops"); + + CAmount nValueOut = tx.GetValueOut(); + CAmount nFees = nValueIn-nValueOut; + double dPriority = view.GetPriority(tx, chainActive.Height()); + + CTxMemPoolEntry entry(tx, nFees, GetTime(), dPriority, chainActive.Height(), mempool.HasNoInputsOf(tx)); + unsigned int nSize = entry.GetTxSize(); + + // Don't accept it if it can't get into a block + CAmount txMinFee = GetMinRelayFee(tx, nSize, true); + if (fLimitFree && nFees < txMinFee) + return state.DoS(0, error("AcceptToMemoryPool: not enough fees %s, %d < %d", + hash.ToString(), nFees, txMinFee), + REJECT_INSUFFICIENTFEE, "insufficient fee"); + + // Require that free transactions have sufficient priority to be mined in the next block. + if (GetBoolArg("-relaypriority", true) && nFees < ::minRelayTxFee.GetFee(nSize) && !AllowFree(view.GetPriority(tx, chainActive.Height() + 1))) { + return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "insufficient priority"); + } + + // Continuously rate-limit free (really, very-low-fee) transactions + // This mitigates 'penny-flooding' -- sending thousands of free transactions just to + // be annoying or make others' transactions take longer to confirm. + if (fLimitFree && nFees < ::minRelayTxFee.GetFee(nSize)) + { + static CCriticalSection csFreeLimiter; + static double dFreeCount; + static int64_t nLastTime; + int64_t nNow = GetTime(); + + LOCK(csFreeLimiter); + + // Use an exponentially decaying ~10-minute window: + dFreeCount \*= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime)); + nLastTime = nNow; + // -limitfreerelay unit is thousand-bytes-per-minute + // At default rate it would take over a month to fill 1GB + if (dFreeCount >= GetArg("-limitfreerelay", 15)*10*1000) + return state.DoS(0, error("AcceptToMemoryPool: free transaction rejected by rate limiter"), + REJECT_INSUFFICIENTFEE, "rate limited free transaction"); + LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize); + dFreeCount += nSize; + } + + if (fRejectAbsurdFee && nFees > ::minRelayTxFee.GetFee(nSize) * 10000) + return error("AcceptToMemoryPool: absurdly high fees %s, %d > %d", + hash.ToString(), + nFees, ::minRelayTxFee.GetFee(nSize) * 10000); + + // Check against previous transactions + // This is done last to help prevent CPU exhaustion denial-of-service attacks. + if (!CheckInputs(tx, state, view, true, STANDARD_SCRIPT_VERIFY_FLAGS, true)) + { + return error("AcceptToMemoryPool: ConnectInputs failed %s", hash.ToString()); + } + + // Check again against just the consensus-critical mandatory script + // verification flags, in case of bugs in the standard flags that cause + // transactions to pass as valid when they're actually invalid. For + // instance the STRICTENC flag was incorrectly allowing certain + // CHECKSIG NOT scripts to pass, even though they were invalid. + // + // There is a similar check in CreateNewBlock() to prevent creating + // invalid blocks, however allowing such transactions into the mempool + // can be exploited as a DoS attack. + if (!CheckInputs(tx, state, view, true, MANDATORY_SCRIPT_VERIFY_FLAGS, true)) + { + return error("AcceptToMemoryPool: BUG! PLEASE REPORT THIS! ConnectInputs failed against MANDATORY but not STANDARD flags %s", hash.ToString()); + } + + // Store transaction in memory + pool.addUnchecked(hash, entry, !IsInitialBlockDownload()); + } + + SyncWithWallets(tx, NULL); + + return true; + } + ``` + +
交易在这里基本就加到了内存池中,这样,如果挖矿成功,就可以打包到区块中。 +
在下一章高级交易里会详细说一下交易内部的实现。 +
+
+
diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\272\224\347\275\221\347\273\234.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\272\224\347\275\221\347\273\234.md" new file mode 100644 index 0000000..b1f4e73 --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\344\272\224\347\275\221\347\273\234.md" @@ -0,0 +1,303 @@ +# 精通比特币之五网络 + +
+ +## 一、比特币网络的架构和算法 + +
+比特币的网络主要以P2P网络协议为主,但也有其它的,比如挖矿的Stratum协议。这里主要以分析P2P网络为主。P2P网络的出现已经有很长一段时间,比较有名的P2P网络包括电骡和BitTorrent.P2P网络一般来说可以分成两类,即结构性和非结构性的,不过最新的也有二者混合在一起的。 +
+P2P网络的路由从最初的洪泛到现在广泛使用的算法有以下几种: +
+ +1、CAN +
+CAN的哈希算法与一致性哈希有所不同。Chord中,哈希得到的键值总是一维的,而在CAN中,哈希的结果由d维的笛卡尔空间来表示。d是一个由系统规模决定的常量。 +
+ +2、Pastry +
+Pastry使用一致性哈希作为哈希算法。哈希所得的键值为一维(实际上使用的是128bit的整数空间)。Pastry也没有规定具体应该采用何种哈希算法。它也是DHT的一个变种。 +
+ +3、Tapestry +
+Tapestry提供了一个分布式容错查找和路由基础平台 。Tapestry的思想来源于Plaxton。在Plaxton中,结点使用自己所知道的邻近结点表,按照目的ID来逐步传递消息。Tapestry基于Plaxtion的思想,加入了容错机制,从而可适应P2P的动态变化的特点. +
+ +4、chord +
+Chord通过把Node和Key映射到相同的空间而保证一致性哈希,为了保证哈希的非重复性,Chord选择SHA-1作为哈希函数,SHA-1会产生一个2160的空间,每项为一个16字节(160bit)的大整数。我们可以认为这些整数首尾相连形成一个环,称之为Chord环。整数在Chord环上按大小顺时针排列,Node(机器的IP地址和Port)与Key(资源标识)都被哈希到Chord环上,这样我们就假定了整个P2P网络的状态为一个虚拟的环,因此我们说Chord是结构化的P2P网络。 +
+ +5、电骡和BT使用的Kademlia +
+二者都实现了后者,但在KEY,VALUE和NODE ID的计算方式有所不同。Kademlia假设网络是一棵二叉树树所有的节点均为叶子节点。在此基础上计算哈希。它属于非集中式的结构算法。 + +
+ +## 二、比特币网络中的结点构成 + +
+ +1、节点功能 + +
+ +![bitcoin-node](img/bitcoin-node.png) +
+节点的功能主要有四个,钱包,挖矿,全节点数据库,路由。 +
+2、节点类型 +
+ +![type](img/node-detail.png) +
+由上图可以看到节点的类型主要有以下几类:全节点,全节点数据库,独立矿工,SPV轻量级节点,矿池服务节点,挖矿节点,stratum SPV节点。 +
+ +## 三、比特币网络中的结点发现 + +
+网络结点的发现,有两种方法,一种是DNS种子的查询,比特币网络本身带有五种DNS种子的名称。第二种方法是带--seednode参数。下面是硬编码在CMainParams中的种子节点。 +
+ +``` C++ +vSeeds.emplace_back("seed.bitcoin.sipa.be"); // Pieter Wuille, only supports x1, x5, x9, and xd +vSeeds.emplace_back("dnsseed.bluematt.me"); // Matt Corallo, only supports x9 +vSeeds.emplace_back("dnsseed.bitcoin.dashjr.org"); // Luke Dashjr +vSeeds.emplace_back("seed.bitcoinstats.com"); // Christian Decker, supports x1 - xf +vSeeds.emplace_back("seed.bitcoin.jonasschnelli.ch"); // Jonas Schnelli, only supports x1, x5, x9, and xd +vSeeds.emplace_back("seed.btc.petertodd.org"); // Peter Todd, only supports x1, x5, x9, and xd +vSeeds.emplace_back("seed.bitcoin.sprovoost.nl"); // Sjors Provoost +``` +
+在新的版本中封装了一个CConnman类型,来管理P2P网络的相关运行。这个和以前有一个比较大的变动。从整体看来,比特币的代码正在朝着专业人士的方向前进。在AppInitMain函数中的第六步中调用了这个类对象的初始化和相关的设置。 +
+ +``` C++ +g_connman = std::unique_ptr(new CConnman(GetRand(std::numeric_limits::max()), GetRand(std::numeric_limits::max()))); +CConnman& connman = \*g_connman; + +peerLogic.reset(new PeerLogicValidation(&connman, scheduler)); +RegisterValidationInterface(peerLogic.get()); +``` +
+在第十一步,启动节点时,才真正开始网络服务,发现并连接相关对等节点,并与之进行通信。Discover()然后connman.Start(scheduler, connOptions),节点就正式启动了。根据实际的参数配置情况会相应启动下列的几个线程: +
+CConnman::ThreadSocketHandler +
+ +``` C++ +void CConnman::ThreadSocketHandler() +{ + unsigned int nPrevNodeCount = 0; + while (!interruptNet) + { +...... + int nSelect = select(have_fds ? hSocketMax + 1 : 0, + &fdsetRecv, &fdsetSend, &fdsetError, &timeout); +...... + + // + // Accept new connections接收新连接 + // + for (const ListenSocket& hListenSocket : vhListenSocket) + { + if (hListenSocket.socket != INVALID_SOCKET && FD_ISSET(hListenSocket.socket, &fdsetRecv)) + { + AcceptConnection(hListenSocket); + } + } + + // + // Service each socket遍历增加引用 + // + std::vector vNodesCopy; + { + LOCK(cs_vNodes); + vNodesCopy = vNodes; + for (CNode* pnode : vNodesCopy) + pnode->AddRef(); + } + for (CNode* pnode : vNodesCopy) + { + if (interruptNet) + return; + + // + // Receive//接收数据 + // +...... + if (recvSet || errorSet) + { + // typical socket buffer is 8K-64K + char pchBuf[0x10000]; + int nBytes = 0; + { + LOCK(pnode->cs_hSocket); + if (pnode->hSocket == INVALID_SOCKET) + continue; + nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); + } +...... + } + + // + // Send 发送数据 + // + if (sendSet) + { + LOCK(pnode->cs_vSend); + size_t nBytes = SocketSendData(pnode); + if (nBytes) { + RecordBytesSent(nBytes); + } + } + +.... + } + } +} +``` +
+如果有网络编程的经验就非常简单了,一个标准的Select调用,辅助大量的错误和异常判断,这里都删除了。 +
+ThreadDNSAddressSeed,这个比较简单,就不贴代码了,之所以启动一个线程是因为在某种情况下要阻塞11秒。其它就是对种子的地址处理然后保存到队列中去。 +
+ThreadOpenAddedConnections和ThreadOpenConnections都是打开网络连接,一个是从RPC -addnode指定的节点连接,一个是地址管理器得到的节点连接。都是调用OpenNetworkConnection这个函数。 + +
+ThreadMessageHandler在交易中进行了比较详细的说明,这里只要简单的说明一下就可以了: +
+ +``` C++ +void CConnman::ThreadMessageHandler() +{ + while (!flagInterruptMsgProc) + { +...... + + for (CNode* pnode : vNodesCopy) + { +...... + // Receive messages + bool fMoreNodeWork = m_msgproc->ProcessMessages(pnode, flagInterruptMsgProc); + fMoreWork |= (fMoreNodeWork && !pnode->fPauseSend); + if (flagInterruptMsgProc) + return; + // Send messages + { + LOCK(pnode->cs_sendProcessing); + m_msgproc->SendMessages(pnode, flagInterruptMsgProc); + } + + if (flagInterruptMsgProc) + return; + } + } +...... +} + +``` +
+两个消息处理函数,ProcessMessages和SendMessages。内部在交易中有过介绍,其它和交易都差不太多。 +
+ +## 四、交换库存 + +
+这个其实就是对等节点比较自己和接收到的请求之间的顶点的差距,做一个深度500的限制的遍历。没有太复杂的情况 。 +
+ +``` C++ +else if (strCommand == NetMsgType::GETDATA) +{ + std::vector vInv; + vRecv >> vInv; + if (vInv.size() > MAX_INV_SZ) + { + LOCK(cs_main); + Misbehaving(pfrom->GetId(), 20, strprintf("message getdata size() = %u", vInv.size())); + return false; + } +...... + pfrom->vRecvGetData.insert(pfrom->vRecvGetData.end(), vInv.begin(), vInv.end()); + ProcessGetData(pfrom, chainparams.GetConsensus(), connman, interruptMsgProc); +} +else if (strCommand == NetMsgType::GETBLOCKS) +{ +...... + LOCK(cs_main); + + // Find the last block the caller has in the main chain + const CBlockIndex* pindex = FindForkInGlobalIndex(chainActive, locator); + + // Send the rest of the chain + if (pindex) + pindex = chainActive.Next(pindex); + int nLimit = 500; +...... + for (; pindex; pindex = chainActive.Next(pindex)) + { + if (pindex->GetBlockHash() == hashStop) + { + LogPrint(BCLog::NET, " getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString()); + break; + } + // If pruning, don't inv blocks unless we have on disk and are likely to still have + // for some reasonable time window (1 hour) that block relay might require. + const int nPrunedBlocksLikelyToHave = MIN_BLOCKS_TO_KEEP - 3600 / chainparams.GetConsensus().nPowTargetSpacing; + if (fPruneMode && (!(pindex->nStatus & BLOCK_HAVE_DATA) || pindex->nHeight <= chainActive.Tip()->nHeight - nPrunedBlocksLikelyToHave)) + { + LogPrint(BCLog::NET, " getblocks stopping, pruned or too old block at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString()); + break; + } + pfrom->PushInventory(CInv(MSG_BLOCK, pindex->GetBlockHash())); + if (--nLimit <= 0) + { + // When this block is requested, we'll send an inv that'll + // trigger the peer to getblocks the next batch of inventory. + LogPrint(BCLog::NET, " getblocks stopping at limit %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString()); + pfrom->hashContinue = pindex->GetBlockHash(); + break; + } + } +} +``` +
+ +## 五、SPV + +
+现在的轻量级钱包基本不会下载区块的所有部分,那个太占用空间和时间了,一般都是只把区块头下载下来,然后对交易用默克尔树进行验证。当然,为了安全起见,SPV节点会连接随机的多个全节点来进行验证,防止意外攻击和双重支付等恶意行为。 +
+Merkle 树的部分在下一章区块链中再阐述。 +
+ +## 六、bloom过滤器及应用 + +
+Bloom过滤器是一个允许用户描述特定的关键词组合而不必精确表述的基于概率的过滤方法。它能让用户在有效搜索关键词的同时保护他们的隐私。在SPV节点里,这一方法被用来向对等节点发送交易信息查询请求,同时交易地址不会被暴露。 +
+布隆过滤器的关键参数如下: +
+ +![bllom](img/bloom.png) + +
+spv节点通过不断发送BLOOM过滤器的消息,来达到获取自己需要数据的目的。如果不再使用后,可以发送清除消息,将过滤器去除。 +
+ +## 七、加密和认证 + +
+比特币的网络可以使用用洋葱路由网络来达到遮蔽隐私的目的。为了安全起见,也可以使用加密通信以及强制要求身份认证,这在BIP-150和BIP-151提案中得到了支持。 + +
+ +## 八、交易池 + +
+交易池在交易的时候儿也说过,这里简单的说明一下,交易池一方面为打包交易到区块中做数据准备,一方面可以通过VIEW映射UTXO到相关需要中去,比如钱包。书中提到的UTXO池,一般在全节点都会维护一个,他其实是一个CCoinsViewCache 的对象,分成两部分,一部分从本地UTXO数据库中获得并映射到CCoinsViewDB ,另外一部分就是刚刚提到的CTxMemPool映射过来的CCoinsViewMemPool 。 +
diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\205\255\345\214\272\345\235\227\351\223\276.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\205\255\345\214\272\345\235\227\351\223\276.md" new file mode 100644 index 0000000..48ec869 --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\205\255\345\214\272\345\235\227\351\223\276.md" @@ -0,0 +1,348 @@ +# 精通比特币之六区块链 + +
+ +## 一、区块 + +
+ +区块链之所以称做区块链,其实就是一个类似链表的东西,新区块通过记载父区块的哈希,以此来不断的迭代形成一条链状的数据结构。先看一下区块的数据结构: +
+ +``` c++ + +class CBlockHeader +{ +public: + // header + static const int32_t CURRENT_VERSION=4; + int32_t nVersion; + uint256 hashPrevBlock; + uint256 hashMerkleRoot; + uint32_t nTime;//时间戳 + uint32_t nBits;//难度 + uint32_t nNonce;//计算HASH的临时数 + + CBlockHeader() + { + SetNull(); + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream\& s, Operation ser_action, int nType, int nVersion) { + READWRITE(this->nVersion); + nVersion = this->nVersion; + READWRITE(hashPrevBlock); + READWRITE(hashMerkleRoot); + READWRITE(nTime); + READWRITE(nBits); + READWRITE(nNonce); + } + + void SetNull() + { + nVersion = CBlockHeader::CURRENT_VERSION; + hashPrevBlock.SetNull(); + hashMerkleRoot.SetNull(); + nTime = 0; + nBits = 0; + nNonce = 0; + } + + bool IsNull() const + { + return (nBits == 0); + } + + uint256 GetHash() const; + //注释掉的原因有特殊字符 fjf 显示不正确 + //int64_t GetBlockTime() const{return (int64_t)nTime;} +}; +class CBlock : public CBlockHeader +{ +public: + // network and disk + std::vector vtx; + + // memory only + mutable std::vector vMerkleTree; + + CBlock() + { + SetNull(); + } + + CBlock(const CBlockHeader &header) + { + SetNull(); + *((CBlockHeader*)this) = header; + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { + READWRITE(*(CBlockHeader*)this); + READWRITE(vtx); + } + + void SetNull() + { + CBlockHeader::SetNull(); + vtx.clear(); + vMerkleTree.clear(); + } + + CBlockHeader GetBlockHeader() const + { + CBlockHeader block; + block.nVersion = nVersion; + block.hashPrevBlock = hashPrevBlock; + block.hashMerkleRoot = hashMerkleRoot; + block.nTime = nTime; + block.nBits = nBits; + block.nNonce = nNonce; + return block; + } + + // Build the in-memory merkle tree for this block and return the merkle root. + // If non-NULL, \*mutated is set to whether mutation was detected in the merkle + // tree (a duplication of transactions in the block leading to an identical merkle root). + uint256 BuildMerkleTree(bool\* mutated = NULL) const; + + std::vector GetMerkleBranch(int nIndex) const; + static uint256 CheckMerkleBranch(uint256 hash, const std::vector& vMerkleBranch, int nIndex); + std::string ToString() const; +}; + +``` +
+区块的数据这里不做详细的说明,毕竟书上和各种资料上都有。而且确实也没有啥可讲的。需要说明的是,得到区块有两种方式,一个是区块本身的啥希值,一个是高度。前者其实也就是区块头哈希值,区块本身不保存这个值。 +区块高度也有需要注意的地方,就是同一个高度,可能有多个区块(软分叉和硬分叉)。 +
+那么查找区块和高度怎么办呢?需要另外一个数据结构: +
+ +``` C++ + +/** The block chain is a tree shaped structure starting with the + * genesis block at the root, with each block potentially having multiple + * candidates to be the next block. A blockindex may have multiple pprev pointing + * to it, but at most one of them can be part of the currently active branch. + */ +class CBlockIndex +{ +public: + //! pointer to the hash of the block, if any. Memory is owned by this CBlockIndex + const uint256* phashBlock; + + //! pointer to the index of the predecessor of this block + CBlockIndex* pprev; + + //! pointer to the index of some further predecessor of this block + CBlockIndex* pskip; + + //! height of the entry in the chain. The genesis block has height 0 + int nHeight; + + //! Which # file this block is stored in (blk?????.dat) + int nFile; + + //! Byte offset within blk?????.dat where this block's data is stored + unsigned int nDataPos; + + //! Byte offset within rev?????.dat where this block's undo data is stored + unsigned int nUndoPos; + + //! (memory only) Total amount of work (expected number of hashes) in the chain up to and including this block + arith_uint256 nChainWork; + + //! Number of transactions in this block. + //! Note: in a potential headers-first mode, this number cannot be relied upon + unsigned int nTx; + + //! (memory only) Number of transactions in the chain up to and including this block. + //! This value will be non-zero only if and only if transactions for this block and all its parents are available. + //! Change to 64-bit type when necessary; won't happen before 2030 + unsigned int nChainTx; + + //! Verification status of this block. See enum BlockStatus + uint32_t nStatus; + + //! block header + int32_t nVersion; + uint256 hashMerkleRoot; + uint32_t nTime; + uint32_t nBits; + uint32_t nNonce; + + //! (memory only) Sequential id assigned to distinguish order in which blocks are received. + int32_t nSequenceId; + + //! (memory only) Maximum nTime in the chain up to and including this block. + unsigned int nTimeMax; + + void SetNull() + { + phashBlock = nullptr; + pprev = nullptr; + pskip = nullptr; + nHeight = 0; + nFile = 0; + nDataPos = 0; + nUndoPos = 0; + nChainWork = arith_uint256(); + nTx = 0; + nChainTx = 0; + nStatus = 0; + nSequenceId = 0; + nTimeMax = 0; + + nVersion = 0; + hashMerkleRoot = uint256(); + nTime = 0; + nBits = 0; + nNonce = 0; + } + + CBlockIndex() + { + SetNull(); + } + + explicit CBlockIndex(const CBlockHeader& block) + { + SetNull(); + + nVersion = block.nVersion; + hashMerkleRoot = block.hashMerkleRoot; + nTime = block.nTime; + nBits = block.nBits; + nNonce = block.nNonce; + } + + CDiskBlockPos GetBlockPos() const { + CDiskBlockPos ret; + if (nStatus & BLOCK_HAVE_DATA) { + ret.nFile = nFile; + ret.nPos = nDataPos; + } + return ret; + } + + CDiskBlockPos GetUndoPos() const { + CDiskBlockPos ret; + if (nStatus & BLOCK_HAVE_UNDO) { + ret.nFile = nFile; + ret.nPos = nUndoPos; + } + return ret; + } + + CBlockHeader GetBlockHeader() const + { + CBlockHeader block; + block.nVersion = nVersion; + if (pprev) + block.hashPrevBlock = pprev->GetBlockHash(); + block.hashMerkleRoot = hashMerkleRoot; + block.nTime = nTime; + block.nBits = nBits; + block.nNonce = nNonce; + return block; + } + +....... + //! Efficiently find an ancestor of this block. + CBlockIndex* GetAncestor(int height); + const CBlockIndex* GetAncestor(int height) const; +}; +``` +
+需要注意的是CBlockIndex实例仅保存在内存当中。若要将区块索引存入磁盘,则需要其子类CDiskBlockIndex。从网络上得到的区块信息是直接落盘的。所以这时候会对其进行处理,从而更新CBlockIndex + +
+ +## 二、Merkle 树 +
+Merkle树部分主要代码在consensus目前下的merkle.h merkle.cpp两个文件内,分为几种情况来计算,本树的计算方法比较简单,就是两两哈希,直到结果为1,如果起始不是偶数,搞成偶数就可以了。方法是复制最后一个到末尾。 +
+SPV轻量钱包可以调用BlockMerkleBranch来得到结果验证相关的数据。验证的方法是发送消息从全节点得到相关的完整树的相关部分,再用ComputeMerkleRootFromBranch计算相关哈希值与得到的进行匹配验证即可。 +
+代码在 net_processing.cpp 中的INV消息数据中。 +
+ +``` c++ +ProcessMessage消息处理函数中 +bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, int64_t nTimeReceived, const CChainParams& chainparams, CConnman* connman, const std::atomic& interruptMsgProc) +{ +else if (strCommand == NetMsgType::GETDATA) +{ +...... + ProcessGetData(pfrom, chainparams.GetConsensus(), connman, interruptMsgProc); +} +} +ProcessGetData中调用: +void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParams, CConnman* connman, const std::atomic& interruptMsgProc) +{ +if (it != pfrom->vRecvGetData.end() && !pfrom->fPauseSend) { + const CInv &inv = \*it; + if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK || inv.type == MSG_CMPCT_BLOCK || inv.type == MSG_WITNESS_BLOCK) { + it++; + ProcessGetBlockData(pfrom, consensusParams, inv, connman, interruptMsgProc); + } +} +} +void static ProcessGetBlockData(CNode* pfrom, const Consensus::Params& consensusParams, const CInv& inv, CConnman* connman, const std::atomic& interruptMsgProc) +{ + ....... + if (inv.type == MSG_BLOCK) + connman->PushMessage(pfrom, msgMaker.Make(SERIALIZE_TRANSACTION_NO_WITNESS, NetMsgType::BLOCK, *pblock)); + else if (inv.type == MSG_WITNESS_BLOCK) + connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::BLOCK, *pblock)); + else if (inv.type == MSG_FILTERED_BLOCK) + { + bool sendMerkleBlock = false; + CMerkleBlock merkleBlock; + { + LOCK(pfrom->cs_filter); + if (pfrom->pfilter) { + sendMerkleBlock = true; + merkleBlock = CMerkleBlock(*pblock, *pfrom->pfilter); + } + } + if (sendMerkleBlock) { + connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::MERKLEBLOCK, merkleBlock)); + // CMerkleBlock just contains hashes, so also push any transactions in the block the client did not see + // This avoids hurting performance by pointlessly requiring a round-trip + // Note that there is currently no way for a node to request any single transactions we didn't send here - + // they must either disconnect and retry or request the full block. + // Thus, the protocol spec specified allows for us to provide duplicate txn here, + // however we MUST always provide at least what the remote peer needs + typedef std::pair PairType; + for (PairType& pair : merkleBlock.vMatchedTxn) + connman->PushMessage(pfrom, msgMaker.Make(SERIALIZE_TRANSACTION_NO_WITNESS, NetMsgType::TX, *pblock->vtx[pair.first])); + } + // else + // no response + } + else if (inv.type == MSG_CMPCT_BLOCK) + ...... +} +``` +
+SPV钱包拉到数据后,就可以按上面讲的进行验证了。 +
+
+ +## 三、比特币网络的类型 + +
+除了主网,主要有三种辅助网络:使用testnet,测试网络;Segnet—隔离见证测试网络;Regtest--本地区块链 +
启动方法类似: +$ bitcoind -testnet +
+使用方法类似:$ bitcoin-cli -testnet getinfo +
diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\233\233\344\272\244\346\230\223\350\257\246\347\273\206\345\210\206\346\236\220.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\233\233\344\272\244\346\230\223\350\257\246\347\273\206\345\210\206\346\236\220.md" new file mode 100644 index 0000000..e169f53 --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\271\213\345\233\233\344\272\244\346\230\223\350\257\246\347\273\206\345\210\206\346\236\220.md" @@ -0,0 +1,656 @@ + +# 一、交易的基本数据结构 + +
+ +![交易的基本数据结构](img/transaction-1.png) + +
+ +# 二、交易的流程 +
+在整个的交易过程中,CTransaction这个类是基础的数据结构,在网络分发的数据中应用的数据结构最终也是它。一个正常的交易从发起到结束,再到确认,需要一个比较长的时间。交易的过程相对来说比较复杂。这里就按照上面的的一个发送交易,对内部的一些内容展开讨论。 +
+在展开前先说一下两个小的概念,一个是最小传播交易费,一个是创建最小交易的费用,这两个时期版本是相同的,但新的分开了。 +
+ +**判断最小传播交易:CFeeRate minRelayTxFee = CFeeRate(5000); 判断最小创建交易:CFeeRate CWallet::minTxFee = CFeeRate(1000);** +
+ +## 1、准备交易 +
+ +``` c++ +WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl \*coinControl) +{ +...... + //此处主要是对本地添加和维护的收款人列表进行处理。 + QList recipients = transaction.getRecipients(); + std::vector vecSend; + + if(recipients.empty())//接收人为空,退出 + { + return OK; + } +...... + // Pre-check input data for validity + Q_FOREACH(const SendCoinsRecipient &rcp, recipients) + { + if (rcp.fSubtractFeeFromAmount) + fSubtractFeeFromAmount = true; + + if (rcp.paymentRequest.IsInitialized()) + { // PaymentRequest... + CAmount subtotal = 0; + const payments::PaymentDetails& details = rcp.paymentRequest.getDetails(); + + //从收款人列表中得到相关脚本,供后面的发送币使用。 + for (int i = 0; i < details.outputs_size(); i++) + { + const payments::Output& out = details.outputs(i); + if (out.amount() <= 0) continue; + subtotal += out.amount(); + const unsigned char* scriptStr = (const unsigned char*)out.script().data(); + CScript scriptPubKey(scriptStr, scriptStr+out.script().size()); + CAmount nAmount = out.amount(); + CRecipient recipient = {scriptPubKey, nAmount, rcp.fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + } + if (subtotal <= 0) + { + return InvalidAmount; + } + total += subtotal; + } + else + { // User-entered bitcoin address / amount: + if(!validateAddress(rcp.address)) + { + return InvalidAddress; + } + if(rcp.amount <= 0) + { + return InvalidAmount; + } + setAddress.insert(rcp.address); + ++nAddresses; + + CScript scriptPubKey = GetScriptForDestination(CBitcoinAddress(rcp.address.toStdString()).Get()); + CRecipient recipient = {scriptPubKey, rcp.amount, rcp.fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + + total += rcp.amount; + } + } + if(setAddress.size() != nAddresses) + { + return DuplicateAddress; + } + + //查询帐户余额 + CAmount nBalance = getBalance(coinControl); + + if(total > nBalance) + { + return AmountExceedsBalance; + } + + { + LOCK2(cs_main, wallet->cs_wallet); + + //把密钥分配池和钱包绑定 + transaction.newPossibleKeyChange(wallet); + + CAmount nFeeRequired = 0; + int nChangePosRet = -1; + std::string strFailReason; + + //创建交易和密钥 + CWalletTx \*newTx = transaction.getTransaction(); + CReserveKey \*keyChange = transaction.getPossibleKeyChange(); + //创建交易 + bool fCreated = wallet->CreateTransaction(vecSend, \*newTx, \*keyChange, nFeeRequired, nChangePosRet, strFailReason, coinControl); + transaction.setTransactionFee(nFeeRequired); + if (fSubtractFeeFromAmount && fCreated) + transaction.reassignAmounts(nChangePosRet);//重新安排零钱帐户的数据 + + if(!fCreated) + { + if(!fSubtractFeeFromAmount && (total + nFeeRequired) > nBalance) + { + //费用不够 + return SendCoinsReturn(AmountWithFeeExceedsBalance); + } + Q_EMIT message(tr("Send Coins"), QString::fromStdString(strFailReason), + CClientUIInterface::MSG_ERROR); + return TransactionCreationFailed; + } + + // reject absurdly high fee > 0.1 bitcoin 费用太高 + if (nFeeRequired > 10000000) + return AbsurdFee; + } + + return SendCoinsReturn(OK); +} +``` + +
+SendCoinsEntry入口UI: +
+ + ![SendCoinsEntry](img/sendcoin-b.png) + +
+自定义费率: +
+ +![feerate](img/fee-cus.png) + +
+上面的代码在上篇文章中有过但为了清晰,删除了大部分的代码。这次基本贴全了。 +
+ +## 2、创建交易 +
+ +**重点照顾一下CreateTransaction:** +
+ +``` c++ +bool CWallet::CreateTransaction(const vector\& vecSend, + CWalletTx\& wtxNew, CReserveKey\& reservekey, CAmount& nFeeRet, int\& nChangePosRet, std::string\& strFailReason, const CCoinControl\*coinControl) +{ + CAmount nValue = 0; + unsigned int nSubtractFeeFromAmount = 0; + BOOST_FOREACH (const CRecipient& recipient, vecSend) + { + if (nValue < 0 || recipient.nAmount < 0) + { + strFailReason = _("Transaction amounts must be positive"); + return false; + } + nValue += recipient.nAmount; + + if (recipient.fSubtractFeeFromAmount) + nSubtractFeeFromAmount++; + } + if (vecSend.empty() || nValue < 0) + { + strFailReason = _("Transaction amounts must be positive"); + return false; + } + + wtxNew.fTimeReceivedIsTxTime = true; + wtxNew.BindWallet(this); + CMutableTransaction txNew; + + // Discourage fee sniping.防止费用狙击--为处理小BUG,增加块设置,和书上略有不同 + // 最新版本是当前块,意思是说(1~5亿间),大于等于当前块,交易都可用。 + // 书上说的不太清楚。 + // However because of a off-by-one-error in previous versions we need to + // neuter it by setting nLockTime to at least one less than nBestHeight. + // Secondly currently propagation of transactions created for block heights + // corresponding to blocks that were just mined may be iffy - transactions + // aren't re-accepted into the mempool - we additionally neuter the code by + // going ten blocks back. Doesn't yet do anything for sniping, but does act + // to shake out wallet bugs like not showing nLockTime'd transactions at + // all. + txNew.nLockTime = std::max(0, chainActive.Height() - 10); //设置锁定时间 + + // Secondly occasionally randomly pick a nLockTime even further back, so + // that transactions that are delayed after signing for whatever reason, + // e.g. high-latency mix networks and some CoinJoin implementations, have + // better privacy. + if (GetRandInt(10) == 0)//设置锁定时间 + txNew.nLockTime = std::max(0, (int)txNew.nLockTime - GetRandInt(100)); + + assert(txNew.nLockTime <= (unsigned int)chainActive.Height()); + assert(txNew.nLockTime < LOCKTIME_THRESHOLD); + + { + LOCK2(cs_main, cs_wallet); + { + nFeeRet = 0; + while (true) + { + txNew.vin.clear(); + txNew.vout.clear(); + wtxNew.fFromMe = true; + + nChangePosRet = -1; + bool fFirst = true; + + CAmount nTotalValue = nValue; + if (nSubtractFeeFromAmount == 0) + nTotalValue += nFeeRet; + double dPriority = 0; + // vouts to the payees + BOOST_FOREACH (const CRecipient& recipient, vecSend) + { + //这里填充CTxOut + CTxOut txout(recipient.nAmount, recipient.scriptPubKey); + + if (recipient.fSubtractFeeFromAmount) + { + txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient + + if (fFirst) // first receiver pays the remainder not divisible by output count + { + fFirst = false; + txout.nValue -= nFeeRet % nSubtractFeeFromAmount; + } + } + //处理是不是小额交易 + if (txout.IsDust(::minRelayTxFee)) + { + if (recipient.fSubtractFeeFromAmount && nFeeRet > 0) + { + if (txout.nValue < 0) + strFailReason = _("The transaction amount is too small to pay the fee"); + else + strFailReason = _("The transaction amount is too small to send after the fee has been deducted"); + } + else + strFailReason = _("Transaction amount too small"); + return false; + } + txNew.vout.push_back(txout);//加入输出,将txout填充入CWalletTx,继承自CTransaction + } + + // Choose coins to use 选择可以花费的币 + set > setCoins; + CAmount nValueIn = 0; + if (!SelectCoins(nTotalValue, setCoins, nValueIn, coinControl)) + { + strFailReason = _("Insufficient funds"); + return false; + } + //设置币龄相关 + BOOST_FOREACH(PAIRTYPE(const CWalletTx\*, unsigned int) pcoin, setCoins) + { + CAmount nCredit = pcoin.first->vout[pcoin.second].nValue; + //The coin age after the next block (depth+1) is used instead of the current, + //reflecting an assumption the user would accept a bit more delay for + //a chance at a free transaction. + //But mempool inputs might still be in the mempool, so their age stays 0 + //计算块龄,这个跟进入mempool有一定的联系。 + int age = pcoin.first->GetDepthInMainChain(); + if (age != 0) + age += 1; + dPriority += (double)nCredit * age; + } + + CAmount nChange = nValueIn - nValue; + if (nSubtractFeeFromAmount == 0) + nChange -= nFeeRet; + + //处理零钱 + if (nChange > 0) + { + // Fill a vout to ourself + // TODO: pass in scriptChange instead of reservekey so + // change transaction isn't always pay-to-bitcoin-address + //提供更健全的处理机制,即不只是处理比特币地址,也包括各种脚本支持的交易。 + CScript scriptChange; + + // coin control: send change to custom address自定义地址 + if (coinControl && !boost::get(&coinControl->destChange)) + scriptChange = GetScriptForDestination(coinControl->destChange); + + // no coin control: send change to newly generated address没有则产生一个新地址 + else + { + // Note: We use a new key here to keep it from being obvious which side is the change. + // The drawback is that by not reusing a previous key, the change may be lost if a + // backup is restored, if the backup doesn't have the new private key for the change. + // If we reused the old key, it would be possible to add code to look for and + // rediscover unknown transactions that were written with keys of ours to recover + // post-backup change. + + // Reserve a new key pair from key pool + CPubKey vchPubKey; + bool ret; + ret = reservekey.GetReservedKey(vchPubKey); + assert(ret); // should never fail, as we just unlocked + + scriptChange = GetScriptForDestination(vchPubKey.GetID()); + } + + //为零钱创建一个输出 + CTxOut newTxOut(nChange, scriptChange); + + // We do not move dust-change to fees, because the sender would end up paying more than requested. + // This would be against the purpose of the all-inclusive feature. + // So instead we raise the change and deduct from the recipient. + //我的理解可能是虽然用记可以多付,但是你不能多收,所以要从你那把钱放回到零钱中去。 + if (nSubtractFeeFromAmount > 0 && newTxOut.IsDust(::minRelayTxFee)) + { + CAmount nDust = newTxOut.GetDustThreshold(::minRelayTxFee) - newTxOut.nValue; + newTxOut.nValue += nDust; // raise change until no more dust + for (unsigned int i = 0; i < vecSend.size(); i++) // subtract from first recipient + { + if (vecSend[i].fSubtractFeeFromAmount) + { + txNew.vout[i].nValue -= nDust; + if (txNew.vout[i].IsDust(::minRelayTxFee)) + { + strFailReason = _("The transaction amount is too small to send after the fee has been deducted"); + return false; + } + break; + } + } + } + + // Never create dust outputs; if we would, just + // add the dust to the fee. + if (newTxOut.IsDust(::minRelayTxFee)) + { + nFeeRet += nChange; + reservekey.ReturnKey(); + } + else + { + // Insert change txn at random position: + nChangePosRet = GetRandInt(txNew.vout.size()+1); + vector::iterator position = txNew.vout.begin()+nChangePosRet;//nChangePosRet 是向量长度的随机数取余。 + txNew.vout.insert(position, newTxOut); //随机插入零钱交易的位置 + } + } + else + reservekey.ReturnKey(); + + // Fill vin + // + // Note how the sequence number is set to max()-1 so that the + // nLockTime set above actually works.此处处理上面类图中的CTxIn + //包括内部数据的填充其中有COutPoint类型的prevout + BOOST_FOREACH(const PAIRTYPE(const CWalletTx\*,unsigned int)& coin, setCoins) + txNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second,CScript(), + std::numeric_limits::max()-1)); + + // Sign签名 + int nIn = 0; + BOOST_FOREACH(const PAIRTYPE(const CWalletTx\*,unsigned int)& coin, setCoins) + if (!SignSignature(\*this, \*coin.first, txNew, nIn++)) + { + strFailReason = _("Signing transaction failed"); + return false; + } + + // Embed the constructed transaction data in wtxNew.生成交易数据 + \*static_cast(&wtxNew) = CTransaction(txNew); + + // Limit size控制大小 不多于 + unsigned int nBytes = ::GetSerializeSize(\*(CTransaction\*)&wtxNew, SER_NETWORK, PROTOCOL_VERSION); + if (nBytes >= MAX_STANDARD_TX_SIZE) + { + strFailReason = _("Transaction too large"); + return false; + } + dPriority = wtxNew.ComputePriority(dPriority, nBytes); + + // Can we complete this as a free transaction?是不是允许成为一个免费的交易 + if (fSendFreeTransactions && nBytes <= MAX_FREE_TRANSACTION_CREATE_SIZE) + { + // Not enough fee: enough priority? + double dPriorityNeeded = mempool.estimatePriority(nTxConfirmTarget); + // Not enough mempool history to estimate: use hard-coded AllowFree. + if (dPriorityNeeded <= 0 && AllowFree(dPriority)) + break; + + // Small enough, and priority high enough, to send for free + if (dPriorityNeeded > 0 && dPriority >= dPriorityNeeded) + break; + } + + CAmount nFeeNeeded = GetMinimumFee(nBytes, nTxConfirmTarget, mempool); + + // If we made it here and we aren't even able to meet the relay fee on the next pass, give up + // because we must be at the maximum allowed fee. + //小于广播交易费用报错。 + if (nFeeNeeded < ::minRelayTxFee.GetFee(nBytes)) + { + strFailReason = _("Transaction too large for fee policy"); + return false; + } + + if (nFeeRet >= nFeeNeeded)//正常成功然后中断循环 + break; // Done, enough fee included. + + // Include more fee and try again.如果找的输入得出的交易费太小就直接回到初始位置重找重算。 + nFeeRet = nFeeNeeded; + continue; + } + } + } + + return true; +} +``` +
+ +## 3、签名 + +
+最后就是签名了: +
+ +``` c++ +两个重载的SignSignature,最重要的是创建了签名的TransactionSignatureCreator类型的creator + +bool ProduceSignature(const BaseSignatureCreator& creator, const CScript& fromPubKey, CScript& scriptSig) +{ + txnouttype whichType; + if (!SignStep(creator, fromPubKey, scriptSig, whichType)) + return false; + + if (whichType == TX_SCRIPTHASH) + { +...... + bool fSolved = + SignStep(creator, subscript, scriptSig, subType) && subType != TX_SCRIPTHASH; +...... + } + + // Test solution + return VerifyScript(scriptSig, fromPubKey, STANDARD_SCRIPT_VERIFY_FLAGS, creator.Checker()); +} +static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptPubKey, + CScript& scriptSigRet, txnouttype& whichTypeRet) +{ + scriptSigRet.clear(); + + vector vSolutions; + if (!Solver(scriptPubKey, whichTypeRet, vSolutions)) + return false; + + CKeyID keyID; + switch (whichTypeRet) + { + case TX_NONSTANDARD: + case TX_NULL_DATA: + return false; + case TX_PUBKEY: + keyID = CPubKey(vSolutions[0]).GetID(); + return Sign1(keyID, creator, scriptPubKey, scriptSigRet); + case TX_PUBKEYHASH: + keyID = CKeyID(uint160(vSolutions[0])); + if (!Sign1(keyID, creator, scriptPubKey, scriptSigRet)) + return false; + else + { + CPubKey vch; + creator.KeyStore().GetPubKey(keyID, vch); + scriptSigRet << ToByteVector(vch); + } + return true; + case TX_SCRIPTHASH: + return creator.KeyStore().GetCScript(uint160(vSolutions[0]), scriptSigRet); + + case TX_MULTISIG: + scriptSigRet << OP_0; // workaround CHECKMULTISIG bug + return (SignN(vSolutions, creator, scriptPubKey, scriptSigRet)); + } + return false; +} + +//返回一个公钥或者哈希 +bool Solver(const CScript& scriptPubKey, txnouttype& typeRet, vector >& vSolutionsRet) +{ + // Templates + static multimap mTemplates; + if (mTemplates.empty()) + { + // Standard tx, sender provides pubkey, receiver adds signature + mTemplates.insert(make_pair(TX_PUBKEY, CScript() << OP_PUBKEY << OP_CHECKSIG)); + + // Bitcoin address tx, sender provides hash of pubkey, receiver provides signature and pubkey + mTemplates.insert(make_pair(TX_PUBKEYHASH, CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG)); + + // Sender provides N pubkeys, receivers provides M signatures + mTemplates.insert(make_pair(TX_MULTISIG, CScript() << OP_SMALLINTEGER << OP_PUBKEYS << OP_SMALLINTEGER << OP_CHECKMULTISIG)); + + // Empty, provably prunable, data-carrying output + if (GetBoolArg("-datacarrier", true)) + mTemplates.insert(make_pair(TX_NULL_DATA, CScript() << OP_RETURN << OP_SMALLDATA)); + mTemplates.insert(make_pair(TX_NULL_DATA, CScript() << OP_RETURN)); + } + + // Shortcut for pay-to-script-hash, which are more constrained than the other types: + // it is always OP_HASH160 20 [20 byte hash] OP_EQUAL + if (scriptPubKey.IsPayToScriptHash()) + { + typeRet = TX_SCRIPTHASH; + vector hashBytes(scriptPubKey.begin()+2, scriptPubKey.begin()+22); + vSolutionsRet.push_back(hashBytes); + return true; + } + + // Scan templates + const CScript& script1 = scriptPubKey; + BOOST_FOREACH(const PAIRTYPE(txnouttype, CScript)& tplate, mTemplates) + { + const CScript& script2 = tplate.second; + vSolutionsRet.clear(); + + opcodetype opcode1, opcode2; + vector vch1, vch2; + + // Compare + CScript::const_iterator pc1 = script1.begin(); + CScript::const_iterator pc2 = script2.begin(); + while (true) + { + if (pc1 == script1.end() && pc2 == script2.end()) + { + // Found a match + typeRet = tplate.first; + if (typeRet == TX_MULTISIG) + { + // Additional checks for TX_MULTISIG: + unsigned char m = vSolutionsRet.front()[0]; + unsigned char n = vSolutionsRet.back()[0]; + if (m < 1 || n < 1 || m > n || vSolutionsRet.size()-2 != n) + return false; + } + return true; + } + if (!script1.GetOp(pc1, opcode1, vch1)) + break; + if (!script2.GetOp(pc2, opcode2, vch2)) + break; + + // Template matching opcodes: + if (opcode2 == OP_PUBKEYS) + { + while (vch1.size() >= 33 && vch1.size() <= 65) + { + vSolutionsRet.push_back(vch1); + if (!script1.GetOp(pc1, opcode1, vch1)) + break; + } + if (!script2.GetOp(pc2, opcode2, vch2)) + break; + // Normal situation is to fall through + // to other if/else statements + } + + if (opcode2 == OP_PUBKEY) + { + if (vch1.size() < 33 || vch1.size() > 65) + break; + vSolutionsRet.push_back(vch1); + } + else if (opcode2 == OP_PUBKEYHASH) + { + if (vch1.size() != sizeof(uint160)) + break; + vSolutionsRet.push_back(vch1); + } + else if (opcode2 == OP_SMALLINTEGER) + { // Single-byte small integer pushed onto vSolutions + if (opcode1 == OP_0 || + (opcode1 >= OP_1 && opcode1 <= OP_16)) + { + char n = (char)CScript::DecodeOP_N(opcode1); + vSolutionsRet.push_back(valtype(1, n)); + } + else + break; + } + else if (opcode2 == OP_SMALLDATA) + { + // small pushdata, <= nMaxDatacarrierBytes + if (vch1.size() > nMaxDatacarrierBytes) + break; + } + else if (opcode1 != opcode2 || vch1 != vch2) + { + // Others must match exactly + break; + } + } + } + + vSolutionsRet.clear(); + typeRet = TX_NONSTANDARD; + return false; +} + +static bool Sign1(const CKeyID& address, const BaseSignatureCreator& creator, const CScript& scriptCode, CScript& scriptSigRet) +{ +...... + if (!creator.CreateSig(vchSig, address, scriptCode)) + return false; +...... +} + +static bool SignN(const vector& multisigdata, const BaseSignatureCreator& creator, const CScript& scriptCode, CScript& scriptSigRet) +{ +...... + CKeyID keyID = CPubKey(pubkey).GetID(); + if (Sign1(keyID, creator, scriptCode, scriptSigRet)) + ++nSigned; +...... + return nSigned==nRequired; +} + +真正的签名在这Creator中: +bool TransactionSignatureCreator::CreateSig(std::vector& vchSig, const CKeyID& address, const CScript& scriptCode) const +{ +...... + uint256 hash = SignatureHash(scriptCode, \*txTo, nIn, nHashType); +...... +} + +uint256 SignatureHash(const CScript& scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType) +{ + static const uint256 one(uint256S("0000000000000000000000000000000000000000000000000000000000000001")); +..... + return ss.GetHash(); +} +``` +
+到现在为止,基本上一个交易的细节就完成了。 diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\272\214.docx" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\272\214.docx" new file mode 100644 index 0000000..c0f1ae7 Binary files /dev/null and "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\344\272\214.docx" differ diff --git "a/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\347\233\270\345\205\263\347\216\257\345\242\203\345\256\211\350\243\205_.md" "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\347\233\270\345\205\263\347\216\257\345\242\203\345\256\211\350\243\205_.md" new file mode 100644 index 0000000..1790ed0 --- /dev/null +++ "b/bitcoin/\347\262\276\351\200\232\346\257\224\347\211\271\345\270\201\347\233\270\345\205\263\347\216\257\345\242\203\345\256\211\350\243\205_.md" @@ -0,0 +1,146 @@ +# 《精通比特币》相关环境安装 + +**一、Centos7编译** + +**1.安装相关依赖** + +yum install -y boost-devel qt-devel protobuf-devel qrencode-devel libevent-devel libtool openssl-devel glibc-headers gcc-c++ +**2.安装berkeley db** + +这里用的是4.8,从官网上可以下载 +http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index-082944.html + +编译安装: +cd db-4.8.30/build_unix +../dist/configure --enable-cxx +make -j8 +sudo make install + +或者: + +执行:./contrib/install_db4.sh \`pwd\` + +安装完成后有提示: +When compiling bitcoind, run `./configure` in the following way: + + export BDB_PREFIX='/home/fjf/bitcoin/db4' + ./configure BDB_LIBS="-L\${BDB_PREFIX}/lib -ldb_cxx-4.8" BDB_CFLAGS="-I\${BDB_PREFIX}/include" ... + +**3.下载源码** +git clone https://github.com/bitcoin/bitcoin.git + +**4.编译安装:** +第一步: cd /root/bitcoin/bitcoin/ + +第二步: sh autogen.sh + +第三步: ./configure --with-incompatible-bdb --with-gui + +第四步: make install + +**5.注意** + +缺少什么库就直接用命令安装这个库就可以了。BOOST库推荐使用1.57,但自动安装的是1.58. +伯克利的数据库推荐使用4.8. +*** +**二.UBUNTU16安装** + +**1.更新源** + + sudo apt-get update + +**2.安装依赖库** + +sudo apt-get -y install build-essential libtool autotools-dev automake autoconf pkg-config bsdmainutils python3 + +sudo apt-get -y install libssl-dev libboost-all-dev libevent-dev + +sudo apt-get -y install libdb-dev libdb++-dev + +sudo apt-get -y install libminiupnpc-dev libzmq3-dev + +sudo apt-get -y install libqt5gui5 libqt5core5a libqt5dbus5 qttools5-dev qttools5-dev-tools libprotobuf-dev + protobuf-compiler libqrencode-dev + +**3.下载源码** + +git clone https://github.com/bitcoin/bitcoin.git + +**4.安装数据库** + +执行:./contrib/install_db4.sh `pwd` + +**5.配置编译** + +./autogen.sh + +./configure --with-incompatible-bdb --prefix=/data/install/bitcoin + +--with-incompatible-bdb为忽略libdb版本差异 + +make + +make install + +**6.其它编译选项** + +可参考GITHUB相关说明,试举例: + +--disable-wallet不编译钱包 + +--without-gui不编译GUI + +**7.注意** + +如果BOOST库不匹配,请安装推荐的版本(1.57) + +*** +**三.使用的配置命令** + +需要配置环境变量BDB_PREFIX: + +export BDB_CFLAGS=/usr/local/BerkeleyDB.4.8/(注意修改为你具体的安装路径) + +sudo ./configure BDB_LIBS="-L\${BDB_PREFIX}/lib -ldb_cxx-4.8" BDB_CFLAGS="-I\${BDB_PREFIX}/include" --with-incompatible-bdb --with-gui + +*** +**四.《精通比特币》书籍中使用的libbitcoin安装** + +**1.如果是全新安装,请参考** + +https://github.com/libbitcoin/libbitcoin-explorer 安装过程。 + +**2.如果已经编译安装成功比特币源码,只需要执行** + +wget https://raw.githubusercontent.com/libbitcoin/libbitcoin-explorer/version3/install.sh + +chmod +x install.sh + +sudo ./install.sh + +安装过程中可能会出现Zmq版本低(要求 >4.2,而比特币默认安装的为4.1),可使用如下方法解决: + +(注意是环境是ubuntu) + +echo "deb http://download.opensuse.org/repositories/network:/messaging:/zeromq:/release-stable/Debian_9.0/ ./" >> /etc/apt/sources.list + +wget https://download.opensuse.org/repositories/network:/messaging:/zeromq:/release-stable/Debian_9.0/Release.key -O- | sudo apt-key add + +apt-get install libzmq3-dev + +如果使用sudo仍然没有权限,可直接打开sources.list,将第二行中的地址库直接加入到最后。保存。 + +注意:要sudo apt update后方可再使用: + + +sudo apt install libzmq3-dev + +即可自动更新到最新的库。 + +**编译的注意事项** + + +1.CPU要求比较高 + +2.内存不低于4G。 + \ No newline at end of file diff --git "a/bitcoin/\347\273\223\347\202\271\347\256\241\347\220\206\345\231\250 CConnman \344\270\255\347\232\204\347\272\277\347\250\213.md" "b/bitcoin/\347\273\223\347\202\271\347\256\241\347\220\206\345\231\250 CConnman \344\270\255\347\232\204\347\272\277\347\250\213.md" new file mode 100644 index 0000000..3686066 --- /dev/null +++ "b/bitcoin/\347\273\223\347\202\271\347\256\241\347\220\206\345\231\250 CConnman \344\270\255\347\232\204\347\272\277\347\250\213.md" @@ -0,0 +1,461 @@ +## 结点管理器 CConnman 中的线程 + + + +类 CConnman 维护 bitcoind 中所有的连接,并负责消息的接收和发送,整个 bitcoind 中只有一个该类的实例 g_connman。CConnman 中的几个线程负责完成这些工作。 + +一、void CConnman::ThreadDNSAddressSeed() 负责通过域名获取结节 ip 地址。 +```cpp +void CConnman::ThreadDNSAddressSeed() +{ + // goal: only query DNS seeds if address need is acute + // Avoiding DNS seeds when we don't need them improves user privacy by + // creating fewer identifying DNS requests, reduces trust by giving seeds + // less influence on the network topology, and reduces traffic to the seeds. + if ((addrman.size() > 0) && + (!gArgs.GetBoolArg("-forcednsseed", DEFAULT_FORCEDNSSEED))) { + if (!interruptNet.sleep_for(std::chrono::seconds(11))) + return; + + LOCK(cs_vNodes); + int nRelevant = 0; + for (auto pnode : vNodes) { + nRelevant += pnode->fSuccessfullyConnected && !pnode->fFeeler && !pnode->fOneShot && !pnode->m_manual_connection && !pnode->fInbound; + } + if (nRelevant >= 2) { + LogPrintf("P2P peers available. Skipped DNS seeding.\n"); + return; + } + } + + const std::vector &vSeeds = Params().DNSSeeds(); + int found = 0; + + LogPrintf("Loading addresses from DNS seeds (could take a while)\n"); + + for (const std::string &seed : vSeeds) { + if (interruptNet) { + return; + } + if (HaveNameProxy()) { + AddOneShot(seed); + } else { + std::vector vIPs; + std::vector vAdd; + ServiceFlags requiredServiceBits = GetDesirableServiceFlags(NODE_NONE); + std::string host = strprintf("x%x.%s", requiredServiceBits, seed); + CNetAddr resolveSource; + if (!resolveSource.SetInternal(host)) { + continue; + } + if (LookupHost(host.c_str(), vIPs, 0, true)) + { + for (const CNetAddr& ip : vIPs) + { + int nOneDay = 24*3600; + CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); + addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old + vAdd.push_back(addr); + found++; + } + addrman.Add(vAdd, resolveSource); + } else { + // We now avoid directly using results from DNS Seeds which do not support service bit filtering, + // instead using them as a oneshot to get nodes with our desired service bits. + AddOneShot(seed); + } + } + } + + LogPrintf("%d addresses found from DNS seeds\n", found); +} +``` +该线程通过域名查询 ip 地址,并把查询到的 ip 地址存入到 addrman(地址管理器,CAddrMan 类的对象)中。另外,在通过域名查询结点地址前,线程会查看本地是否有已知的结节地址,当前是否有正在通信的结节,程序启动时是否强制通过域名去查询结点地址等来综合考虑是否要去查询结点地址。 + +二、void CConnman::ThreadSocketHandler(); +``` +void CConnman::ThreadSocketHandler() +{ + unsigned int nPrevNodeCount = 0; + while (!interruptNet) + { + // + // Disconnect nodes + // + { + LOCK(cs_vNodes); + // Disconnect unused nodes + std::vector vNodesCopy = vNodes; + for (CNode* pnode : vNodesCopy) + { + if (pnode->fDisconnect) + { + // remove from vNodes + vNodes.erase(remove(vNodes.begin(), vNodes.end(), pnode), vNodes.end()); + + // release outbound grant (if any) + pnode->grantOutbound.Release(); + + // close socket and cleanup + pnode->CloseSocketDisconnect(); + + // hold in disconnected pool until all refs are released + pnode->Release(); + vNodesDisconnected.push_back(pnode); + } + } + } +...... + // + // Find which sockets have data to receive + // + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 50000; // frequency to poll pnode->vSend + + fd_set fdsetRecv; + fd_set fdsetSend; + fd_set fdsetError; + FD_ZERO(&fdsetRecv); + FD_ZERO(&fdsetSend); + FD_ZERO(&fdsetError); + SOCKET hSocketMax = 0; + bool have_fds = false; + + for (const ListenSocket& hListenSocket : vhListenSocket) { + FD_SET(hListenSocket.socket, &fdsetRecv); + hSocketMax = std::max(hSocketMax, hListenSocket.socket); + have_fds = true; + } + + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + { + // Implement the following logic: + // * If there is data to send, select() for sending data. As this only + // happens when optimistic write failed, we choose to first drain the + // write buffer in this case before receiving more. This avoids + // needlessly queueing received data, if the remote peer is not themselves + // receiving data. This means properly utilizing TCP flow control signalling. + // * Otherwise, if there is space left in the receive buffer, select() for + // receiving data. + // * Hand off all complete messages to the processor, to be handled without + // blocking here. + + bool select_recv = !pnode->fPauseRecv; + bool select_send; + { + LOCK(pnode->cs_vSend); + select_send = !pnode->vSendMsg.empty(); + } + + LOCK(pnode->cs_hSocket); + if (pnode->hSocket == INVALID_SOCKET) + continue; + + FD_SET(pnode->hSocket, &fdsetError); + hSocketMax = std::max(hSocketMax, pnode->hSocket); + have_fds = true; + + if (select_send) { + FD_SET(pnode->hSocket, &fdsetSend); + continue; + } + if (select_recv) { + FD_SET(pnode->hSocket, &fdsetRecv); + } + } + } + + int nSelect = select(have_fds ? hSocketMax + 1 : 0, + &fdsetRecv, &fdsetSend, &fdsetError, &timeout); + if (interruptNet) + return; +...... + // + // Accept new connections + // + for (const ListenSocket& hListenSocket : vhListenSocket) + { + if (hListenSocket.socket != INVALID_SOCKET && FD_ISSET(hListenSocket.socket, &fdsetRecv)) + { + AcceptConnection(hListenSocket); + } + } + + // + // Service each socket + // + std::vector vNodesCopy; + { + LOCK(cs_vNodes); + vNodesCopy = vNodes; + for (CNode* pnode : vNodesCopy) + pnode->AddRef(); + } + for (CNode* pnode : vNodesCopy) + { + if (interruptNet) + return; + + // + // Receive + // + bool recvSet = false; + bool sendSet = false; + bool errorSet = false; + { + LOCK(pnode->cs_hSocket); + if (pnode->hSocket == INVALID_SOCKET) + continue; + recvSet = FD_ISSET(pnode->hSocket, &fdsetRecv); + sendSet = FD_ISSET(pnode->hSocket, &fdsetSend); + errorSet = FD_ISSET(pnode->hSocket, &fdsetError); + } + if (recvSet || errorSet) + { + // typical socket buffer is 8K-64K + char pchBuf[0x10000]; + int nBytes = 0; + { + LOCK(pnode->cs_hSocket); + if (pnode->hSocket == INVALID_SOCKET) + continue; + nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); + } + if (nBytes > 0) + { + bool notify = false; + if (!pnode->ReceiveMsgBytes(pchBuf, nBytes, notify)) + pnode->CloseSocketDisconnect(); + RecordBytesRecv(nBytes); + if (notify) { + size_t nSizeAdded = 0; + auto it(pnode->vRecvMsg.begin()); + for (; it != pnode->vRecvMsg.end(); ++it) { + if (!it->complete()) + break; + nSizeAdded += it->vRecv.size() + CMessageHeader::HEADER_SIZE; + } + { + LOCK(pnode->cs_vProcessMsg); + pnode->vProcessMsg.splice(pnode->vProcessMsg.end(), pnode->vRecvMsg, pnode->vRecvMsg.begin(), it); + pnode->nProcessQueueSize += nSizeAdded; + pnode->fPauseRecv = pnode->nProcessQueueSize > nReceiveFloodSize; + } + WakeMessageHandler(); + } + } + else if (nBytes == 0) + { + // socket closed gracefully + if (!pnode->fDisconnect) { + LogPrint(BCLog::NET, "socket closed\n"); + } + pnode->CloseSocketDisconnect(); + } + else if (nBytes < 0) + { + // error + int nErr = WSAGetLastError(); + if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) + { + if (!pnode->fDisconnect) + LogPrintf("socket recv error %s\n", NetworkErrorString(nErr)); + pnode->CloseSocketDisconnect(); + } + } + } + + // + // Send + // + if (sendSet) + { + LOCK(pnode->cs_vSend); + size_t nBytes = SocketSendData(pnode); + if (nBytes) { + RecordBytesSent(nBytes); + } + } + + // + // Inactivity checking + // + int64_t nTime = GetSystemTimeInSeconds(); + if (nTime - pnode->nTimeConnected > 60) + { + if (pnode->nLastRecv == 0 || pnode->nLastSend == 0) + { + LogPrint(BCLog::NET, "socket no message in first 60 seconds, %d %d from %d\n", pnode->nLastRecv != 0, pnode->nLastSend != 0, pnode->GetId()); + pnode->fDisconnect = true; + } + else if (nTime - pnode->nLastSend > TIMEOUT_INTERVAL) + { + LogPrintf("socket sending timeout: %is\n", nTime - pnode->nLastSend); + pnode->fDisconnect = true; + } + else if (nTime - pnode->nLastRecv > (pnode->nVersion > BIP0031_VERSION ? TIMEOUT_INTERVAL : 90*60)) + { + LogPrintf("socket receive timeout: %is\n", nTime - pnode->nLastRecv); + pnode->fDisconnect = true; + } + else if (pnode->nPingNonceSent && pnode->nPingUsecStart + TIMEOUT_INTERVAL * 1000000 < GetTimeMicros()) + { + LogPrintf("ping timeout: %fs\n", 0.000001 * (GetTimeMicros() - pnode->nPingUsecStart)); + pnode->fDisconnect = true; + } + else if (!pnode->fSuccessfullyConnected) + { + LogPrint(BCLog::NET, "version handshake timeout from %d\n", pnode->GetId()); + pnode->fDisconnect = true; + } + } + } + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodesCopy) + pnode->Release(); + } + } +} +``` +这是 CConnman 中最重要的一个线程,ThreadSocketHandler 负责调度向所有连接的结点发送和接收数据。首先从 vNodes(保存了所有已连接的结点)删除已不再使用的结点。接下来再利用系统调用 select 来判断可对哪些 socket 进行发送和接收数据,这些 socket 包括监听 socket 集合 vhListenSocket,和已连接的节点进行通信的 socket。当 vhListenSocket 中的 socket 有读事件时,则调用 AcceptConnection 函数接收新的连接请求,并将新结点信息加入到 vNodes 中。对于 vNodes 中结点对应的 socket,则根据事件类型进行数据发送或数据接收。 + +三、void CConnman::ThreadOpenConnections(const std::vector connect) +``` +void CConnman::ThreadOpenConnections(const std::vector connect) +{ + // Connect to specific addresses + if (!connect.empty()) + { + for (int64_t nLoop = 0;; nLoop++) + { + ProcessOneShot(); + for (const std::string& strAddr : connect) + { + CAddress addr(CService(), NODE_NONE); + OpenNetworkConnection(addr, false, nullptr, strAddr.c_str(), false, false, true); + for (int i = 0; i < 10 && i < nLoop; i++) + { + if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) + return; + } + } + if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) + return; + } + } + + // Initiate network connections + int64_t nStart = GetTime(); + + // Minimum time before next feeler connection (in microseconds). + int64_t nNextFeeler = PoissonNextSend(nStart*1000*1000, FEELER_INTERVAL); + while (!interruptNet) + { + ProcessOneShot(); + + if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) + return; + + CSemaphoreGrant grant(*semOutbound); + if (interruptNet) + return; + + // Add seed nodes if DNS seeds are all down (an infrastructure attack?). + if (addrman.size() == 0 && (GetTime() - nStart > 60)) { + static bool done = false; + if (!done) { + LogPrintf("Adding fixed seed nodes as DNS doesn't seem to be available.\n"); + CNetAddr local; + local.SetInternal("fixedseeds"); + addrman.Add(convertSeed6(Params().FixedSeeds()), local); + done = true; + } + } + + // + // Choose an address to connect to based on most recently seen + // + CAddress addrConnect; + + // Only connect out to one peer per network group (/16 for IPv4). + // Do this here so we don't have to critsect vNodes inside mapAddresses critsect. + int nOutbound = 0; + std::set > setConnected; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) { + if (!pnode->fInbound && !pnode->m_manual_connection) { +...... + setConnected.insert(pnode->addr.GetGroup()); + nOutbound++; + } + } + } +...... + bool fFeeler = false; + + if (nOutbound >= nMaxOutbound && !GetTryNewOutboundPeer()) { + int64_t nTime = GetTimeMicros(); // The current time right now (in microseconds). + if (nTime > nNextFeeler) { + nNextFeeler = PoissonNextSend(nTime, FEELER_INTERVAL); + fFeeler = true; + } else { + continue; + } + } + + int64_t nANow = GetAdjustedTime(); + int nTries = 0; + while (!interruptNet) + { + CAddrInfo addr = addrman.Select(fFeeler); + + // if we selected an invalid address, restart + if (!addr.IsValid() || setConnected.count(addr.GetGroup()) || IsLocal(addr)) + break; +...... + nTries++; + if (nTries > 100) + break; + + if (IsLimited(addr)) + continue; + + // only consider very recently tried nodes after 30 failed attempts + if (nANow - addr.nLastTry < 600 && nTries < 30) + continue; +...... + if (!fFeeler && !HasAllDesirableServiceFlags(addr.nServices)) { + continue; + } else if (fFeeler && !MayHaveUsefulAddressDB(addr.nServices)) { + continue; + } + + // do not allow non-default ports, unless after 50 invalid addresses selected already + if (addr.GetPort() != Params().GetDefaultPort() && nTries < 50) + continue; + + addrConnect = addr; + break; + } + + if (addrConnect.IsValid()) { + + if (fFeeler) { + // Add small amount of random noise before connection to avoid synchronization. + int randsleep = GetRandInt(FEELER_SLEEP_WINDOW * 1000); + if (!interruptNet.sleep_for(std::chrono::milliseconds(randsleep))) + return; + LogPrint(BCLog::NET, "Making feeler connection to %s\n", addrConnect.ToString()); + } + + OpenNetworkConnection(addrConnect, (int)setConnected.size() >= std::min(nMaxConnections - 1, 2), &grant, nullptr, false, fFeeler); + } + } +} +``` +ThreadOpenConnections 负责主动连接新的节点,以保证本地和一定数量的结点保持有连接。首先是连接配置文件中指定的一些结点,另外还会从 addrman 中选择一些结点进行主动连接。 \ No newline at end of file diff --git "a/bitcoin/\351\222\261\345\214\205\347\232\204\345\212\237\350\203\275\347\261\273\345\233\276\345\222\214\346\265\201\347\250\213\345\233\276.md" "b/bitcoin/\351\222\261\345\214\205\347\232\204\345\212\237\350\203\275\347\261\273\345\233\276\345\222\214\346\265\201\347\250\213\345\233\276.md" new file mode 100644 index 0000000..537fe82 --- /dev/null +++ "b/bitcoin/\351\222\261\345\214\205\347\232\204\345\212\237\350\203\275\347\261\273\345\233\276\345\222\214\346\265\201\347\250\213\345\233\276.md" @@ -0,0 +1,12 @@ +# 钱包的功能类图和流程图 +
+一、功能的类图 +
+ +![transaction-class](img/qt-wallet1.png) +
+二、流程图 +
+ +![trans-mempool](img/qt-wallet0.png) +
diff --git a/consensus/.gitkeep b/consensus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/consensus/consensus.2pc.md b/consensus/consensus.2pc.md new file mode 100644 index 0000000..5c4ffcb --- /dev/null +++ b/consensus/consensus.2pc.md @@ -0,0 +1,87 @@ + +# 共识算法之:2PC + +## 引言 + +在分布式系统发展的过程中,对于如何达成不同副本的数据一致性,诞生了很多不同的算法。其中两阶段提交协议(Two-Phase Commit,2PC)是一个非常基础的算法,这个算法最初是随着分布式数据库而发展起来的。实际上这个算法存在着很多问题,不过透彻的理解这个算法,有助于理解共识算法中的许多基本问题,所以本文着重讨论这个算法。在现实生活中,有很多模型与这个类似,所以在这里就以约饭局这种最常见的情形来引入。 + + +## 一个人的饭局 + +这是最简单的情况,我一个人去吃饭,那当然是自己一个人做决定就好了,无论是我想去海底捞,还是来一份兰州拉面,或者是点一份外卖,我自己想吃什么就吃什么,想什么时候吃就什么时候吃,不需要与其他人沟通商量。 + + > 对于计算系统来说,独立的一台计算机上的数据,只要自己存储正确了就可以了,不需要与其他计算机同步。 + + +## 两个人的饭局 + +在这里我们先做一个前提条件的限定,即人和人之间约饭局的沟通方式只能通过短信沟通。 + +有两个人要约着一起吃饭,情况变得稍微不一样了。某天上午一上班,小蓝就给自己的死党小明发短信说,中午一起去吃披萨吧。一个小时过去了,小蓝没有收到任何回复短信,他就想,该死的小明,中午到底要不要去吃披萨呢?你要是不去的话早说呀,我就去约小红了。小蓝又给小明发了一条短信,这次很快就收到了小明回复的短信,OK,中午一起去吃披萨。 + +这个例子中,只有两个人需要通过短信的方式来确认,中午是否一起去吃披萨。由于短信是不可靠的,在发送的过程中有可能丢失,所以在这次约饭局的过程中,小蓝发的第一条短信,小明是否收到了是不确定的。也许小明根本就没有收到第一条短信,或者是小明收到了第一条短信,马上就给小蓝回复了,可是小明回复的短信丢失了,没有正确的发送给小蓝,导致小蓝一个小时都没有收到回复短信。 + +> 对于两台计算机的通讯来说,情况也是类似的,而且这是最基础的网络模型。A通过网络给B发送了一条消息,根本无从知晓B是否收到或者是否处理了。需要B给A回复一条Ack消息,告诉A自己这边收到了。但是回复消息也有可能丢失,所以假如B要求A再给B发一条确认收到回复的消息的话,这条消息本身也有可能丢失。假如B为了确认收到再给A回复确认消息的话,,这种确认消息会无休止的循环下去。。。。。。 + +如果约饭局的人更多会怎么样呢? + + +## 三个人的饭局 + +这时候比两个人约饭局又复杂了一些。小蓝早上同时给两个死党小明和小强发了短信,约他们中午一起去吃披萨。这次小明很快就回了短信,OK。但是不知道什么原因小强却没有回复短信。这个时候小蓝就只有等着,不知道小强那里什么情况。小明这里也纳闷,我都说了要去,到现在还没有消息,中午到底还要不要一起去呢?一个小时之后,小强给小蓝回复了短信,抱歉哈,今天中午领导请大家吃饭,不能和你们一起吃披萨了。小蓝一看,赶紧给小明发短信,今天中午的饭局取消,自己解决吧。 + +在这个例子中,需要三个人对于中午是否一起去吃披萨达成共识。“中午一起去吃披萨”这个建议由小蓝发起,需要获得小明和小强的认可,中午的饭局才能落实。小明很快就确认了“中午一起吃披萨”这个建议,由于某种未知的原因导致小蓝并没有及时收到小强的回复,从而无法确定小强对这个建议的态度,所以小蓝和小明就只能等着,也不能安排其他的饭局。直到一个小时之后,小强才回复消息,无法参加中午的饭局,小蓝一看,中午的饭局告吹了,只好通知小明,取消饭局。 + +上述的情况如果极端一点点,小强根本就没有收到小蓝的短信,或者小强下午才看到小蓝早上的短信,匆匆做出回复,但这时候已经太迟了,午餐时间已经过去了,难道小蓝和小明要一直等着小强吗?当然不会。其实小蓝还有两个选择:1,一个小时没有收到小强确认短信的情况下,再次向小强发送短信,问他中午我们仨一起去吃披萨好不好? 2,在早上发送第一条短信的时候,就告诉小明和小强,如果一个小时之内没有收到你们的确认短信,中午的饭局就取消。 + + +> 对于计算系统来说,三个节点之间确认一个数据和更多节点之间确认一个数据从理论上来看已经没有太大差异了。对于发起建议的节点,专业术语是协调者(Coordinator),对于接收建议的节点,专业术语是参与者(Participant)。在2PC协议中,协调者需要收到所有参与者的确认回复之后,再次通知所有参与者执行建议。只要有一个参与者回复说无法执行这个建议,那么协调者只好通知所有的参与者,取消刚才的建议。这个模型看起来很简单,然而在执行过程中的任意阶段都有可能发生异常,从而导致这个模型无法正常工作。 + +下面,我们以三个人的饭局和2PC协议做对比的方式来解说2PC协议。 + + +注意:在经典的2PC协议中,并没有设置超时机制,这里之所以加入是因为使用了三个人约饭局这样的日常生活中的例子做对比,而我们日常生活中的例子是经常加入超时机制的。所以为了明确的对比和易于理解,这里加入了超时机制。 + +| 序 | 三个人的饭局 | 2PC | 解说 | +| - | :-: |:-: | :-: | +| Prepare阶段 | | | | +| | 小蓝早上同时给两个死党小明和小强发了短信,约他们中午一起去吃披萨,并且设定时间,一个小时没有收到你们的答复,就取消饭局 | 协调者给参与者发送Prepare消息,并且设定一个超时时间 | 在这里,小蓝是协调者(Coordinator)角色,小明和小强都是参与者(Participant)角色 | +| | 正常:小明和小强都收到了预约饭局的短信 | 正常:参与者全部收到了Prepare消息 | 这是最理想的状况,在现实世界中,这也是绝大多数的情况。异常出现的比例还是很低的 | +|| 异常:短信信道出错,小蓝这里直接显示短信发不出去 | 异常:网络出错,协调者发送Prepare消息失败 | 这种情况是最简单的,发送者自身知道消息没有能够发送出去,过一会儿重新发送一遍就好了。我们的日常生活中就是这么干的 | +| | 异常:小蓝这里显示短信发送成功,但是小明和小强至少有一个人没有收到短信。假设小明收到了短信,而小强没有收到短信 | 异常:协调者这里感觉到是消息发送成功,但是参与者并不是全部都收到消息了,至少有一个接收者没有收到消息 | 对于没有收到消息的接收方来说,根本就不知道有这么一个要求,所以当然不会做出任何反应 | +| 反馈 | | | | +| | 收到小蓝短信的人(小明)给小蓝发送回复短信 | 收到协调者消息的参与者给协调者发送反馈消息 | 也只有收到了消息的接收方才能够做出反应 | +| | 异常:短信信道出错,直接显示发送反馈短信失败 | 异常:网络出错,直接显示发送反馈消息失败 | 这种情况也好处理,稍后重试即可 | +| | 异常:小明这里显示短信发送成功,然而实际上小蓝并没有收到反馈短信 | 异常:参与者显示消息发送成功,然而实际上协调者并没有收到反馈消息 | 这个时候就有点儿麻烦了。接收方已经发送反馈了,而且显示成功。但是由于网络原因,协调者并没有收到参与者的反馈? | +| | 异常:小明收到了小蓝的短信,还没有来得及回复,就被领导找去开会,讨论事情去了,根本没机会回复短信 | 异常:参与者收到了协调者的Prepare消息,但是由于同时执行的任务太多,无法及时回复短信。或者宕机而无法回复短信 | 这种情况的发生也很正常,或许是由于硬件或者软件的故障导致宕机,也或许是当时系统正在执行某种极其耗费系统资源的任务,根本无法及时回复 | +| 接收反馈 | | | | +| | 正常:小蓝很快就收到了小明和小强的反馈短信 | 正常:协调者在超时时间内收到了所有参与者的反馈消息 | 这是最理想的情况,也是大多数时候的情况 | +| | 异常:小蓝的手机没电了,根本没收到小明和小强的反馈短信 | 异常:协调者自身宕机了,无法收到参与者的反馈消息 | 对于小蓝来说,手机充电开机之后,还可以收到短信。可是计算机宕机恢复之后,却无法收到之前丢失的消息 | +| | 异常:一个小时过去了,小蓝没有收到任何人的反馈消息 | 超时时间已过,协调者没有收到任何参与者的回复 | 超时时间已过,还有一个可选方案是,再次发送Prepare消息 | +| | 异常:小蓝只收到了小明的反馈。但是一个小时过去了,还没有收到小强的反馈 | 异常:在超时时间之内,协调者没有能够收到所有参与者的反馈 | 没有收到反馈的参与者,可能有几种原因:1,参与者没有收到Prepare消息。2,参与者宕机或者来不及回复。3,回复消息丢失 | +| Commit阶段 | | | | +| | 正常:小蓝收到了小明和小强的短信,确认都可以参加中午的饭局。所以小蓝给小明和小强发出确认短信,确定了中午一起去吃披萨 | 正常:协调者收到了所有参与者发回的反馈消息,这个动作可以执行。于是协调者向所有参与者发送了执行动作的Commit消息 | 这是正常流程,也是我们希望的流程 | +| | 正常:小蓝收到了小明的反馈短信说可以参加饭局,收到了小强的反馈短信,内容是无法参加中午的饭局。这时候小蓝只好给小明和小强都发送取消饭局的短信 | 正常:协调者收到了所有参与者的反馈消息,至少有一个参与者反馈说无法执行这个动作。协调者向所有参与者发送Abort消息 | 协调者只有在收到所有参与者都确认可以执行这个动作的前提下,才能向所有参与者发送Commit消息。只要有一个参与者回复说无法执行,那么整个动作就要取消 | +| | 异常:小蓝只收到了小明的回复短信说可以参加中午的饭局,一个小时过去了也没有收到小强的回复短信。这时小蓝只好向小明和小强都发送短信说,中午的饭局取消 | 异常:协调者在超时时间之内没有收到所有参与者的反馈消息,只好向所有参与者发送Abort消息 | 协调者不能收到所有参与者确认可以执行的反馈,那么就取消这个动作 | +| | 异常:小蓝被突然发生的事情打断,或者睡着了,不能及时给小明和小强发出确认消息 | 异常:协调者宕机,不能及时给参与者发送Commit消息 | 这个情况也比较糟糕。协调者在收到参与者的反馈消息之后,应该立刻把这个消息持久化存储。在宕机恢复之后,再去查看自己的状态信息。不过也有可能是这样,协调者收到了参与者的反馈消息,还没有来得及持久化存储就宕机了。宕机恢复之后也无法知晓收到过参与者的反馈消息,这种情况下,只能当做超时处理了 | +| | 异常:小蓝向小明和小强都发送了确定中午饭局或者取消饭局的短信,但是显示发送失败。这种情况稍后再重发一遍就好了 | 异常:协调者向参与者发送执行或者取消指令的时候,网络不通,发送失败。这种情况下,稍后再重新发送指令就好了 | 很明确是网络不通而发送失败的情况,是比较好处理的,稍后重试吧 | +| | 异常:小蓝向小明和小强都发送了确定中午饭局或者取消饭局的短信,显示发送成功。但是小明或小强并没有收到这个短信 | 异常:协调者向所有参与者发送了执行或者取消的消息,但并不是所有参与者都收到了这条消息 | 这种情况就比较麻烦了。收到消息的参与者很明确的执行了指令,执行或者取消。但是对于没有收到消息的参与者,就无所适从了,只有在超时时间过去之后,自动取消 | +| 结果返回 | | | | +| | - | 参与者执行完成Commit动作之后,向协调者反馈Commit结果 | 饭局这个例子就不需要再次返回结果了,中午三个人一起吃了披萨就是执行结果,都一起吃饭了,就不需要发短信回复结果了,因为小蓝已经知道结果了。然而计算系统却需要回复结果,不然协调者是不可能知道的 | + +## 优缺点 + +- 优点:原理简单,易于理解,易于实现 +- 缺点:同步阻塞,单点故障,数据不一致 + - 注意:2PC协议本身是没有引入超时机制的 + +| 缺点 | 三个人的饭局 | 2PC | 解说 | +| - | :-: |:-: | :-: | +| 同步阻塞 | 在预约饭局的过程中,小蓝、小明、小强三个人,任意一人出现意外情况,比如:睡着了,去开会了等等,其他两人都只能等着,导致整个预约过程无法继续下去 | 协调者和所有的参与者都必须等待其它节点的响应结果,任意一个节点宕机或者网络故障,都会导致其他节点一直等待 | 其中一种解决方案就是引入超时机制 | +| 单点故障 | 小蓝是核心角色,如果小蓝睡着了,整个饭局预约工作便无法推进。更为严重的是,如果小蓝是在第二阶段睡着了,小明和小强无法确定,是要一直等着消息,还是可以自己去食堂吃饭 | 协调者一旦宕机或者协调者的网络断掉,参与者就不知道下一步该怎么办。尤其严重的是,协调者如果在第二阶段发出Commit指令前宕机,参与者还在等待Commit或者Abort指令,没有指令到达,就一直处于资源锁定状态 | 引入超时机制或许是一种解决办法,但是不完善 | +| 数据不一致 | 在小蓝正式向小明和小强发出确定中午一起吃饭的短信之后,如果只有小明收到了短信,而小强没有收到短信,那么小明会去参加中午的饭局,而小强则不会去 | 在第二阶段发出Commit或者Abort指令之后,如果只有部分参与者收到了指令,那么收到指令的参与者与没有收到指令的参与者就会采取不同的动作,导致整体数据不一致 | 这个时候,超时机制也不好使,对于小强来说,他不知道该去参加中午的饭局,还是不该去参加中午的饭局?因为小蓝和小明可能会一起去吃披萨,也有可能不去 | + + +## 小结 + +2PC协议是经典的分布式系统共识算法,虽然存在着很多固有的缺陷,但却为很多实用的共识算法打下了基础。透彻的理解这个算法,有助于更好的理解那些实用的共识算法,比如:Paxos diff --git a/consensus/consensus.basic.md b/consensus/consensus.basic.md new file mode 100644 index 0000000..ef536c8 --- /dev/null +++ b/consensus/consensus.basic.md @@ -0,0 +1,125 @@ + + +# 共识算法之:起源 + + +### 前言 + +在区块链大热的当下,“共识算法”一词被频繁的提及。凡是区块链相关的项目,几乎都会提及共识算法一词,而对于做区块链平台的项目,那就必须要说清楚其使用的共识算法模型,否则不可能获得别人的认可。那么到底什么是共识算法,它是如何产生的,共识算法为什么在区块链项目中拥有如此重要的江湖地位,它的功能到底是什么,区块链是否可以不要共识算法? + +对于共识算法的问题非常多,我们无法在一篇文章中都说清楚。带着这一系列的疑问,本文先探讨第一个问题:共识算法是如何演变而来的?当我们知道了共识算法的成长经历,我们也就可以理解共识算法是为何而生,就可以做到知其然也知其所以然。 + +区块链,从本质上来说是一个分布式系统,所以要了解共识算法,我们先从分布式系统说起。 + + +### 计算系统的演进 + +任何事物的出现,都是为了解决某一个特定的问题。那些不能解决任何问题的事物,是没有生命力的,从而无法长期生存下去。共识算法也是为了解决某一个特定问题而诞生的,要理解这个特定的问题,我们需要从计算系统的演进开始。 + +计算机最初被发明出来的时候,是一台独立的个体,而且在最初几十年的发展历程中,都是以一台一台个体的形式出现的。在这个阶段中,所有数据的输入、处理、存储、输出都在一台计算机上完成。因为数据只存在于一台机器上,所以只要在这一台机器上是正确的,就可以了。如果希望把数据从一台机器上转移到另外一台机器,只能通过人工的方式把数据copy到外置存储设备(比如:软盘甚至是更古老的设备,那个时候还没有U盘),然后再从这个外置存储设备copy到另外一个计算机上去。 + +随着网络的出现,情况变得有些不一样了。数据从一台计算机copy到另外一台计算机,现在可以通过网络传输,而不是必须通过外置存储设备这个中转媒介了。在这个阶段,数据量都还比较小,确保数据从一台计算机传递到另外一台计算机之后不发生变化的方法,只需要确保网络传输正确就可以了。 + +当网络从最初的简单形态发展到互联网时代,尤其是若干互联网巨头的发展,每家巨头都拥有庞大而复杂数据的时候,如何保证数据在不同的计算机上都是相同的这一个问题,已经变成了一个非常复杂的课题。伴随着这一课题的出现,发展出了一个全新的领域,分布式计算。 + +确保数据在不同计算机上相同的课题,有了一个专门的术语,叫做 **数据一致性(Consistency)**。 + +在这个大规模互联网应用阶段,数据一致性的概念也扩展了。从最初的仅仅是把数据从一台计算机传输到另外一台计算机,发展到了在两台或者多台计算机上执行相同序列的一系列指令之后得到的结果也是相同的。 + +如果数据在不同的计算机上不一致,会带来什么样的后果呢,这个要根据应用的类型,以及数据的含义而确定。有些数据无关紧要,而有些数据则至关重要。 + +**举一个例子:** + +我们来考察一个信用卡支付系统在一个水果店的使用场景。由于信用卡系统已经是一个非常庞大的系统,拥有上亿的用户,相应的数据量也极其庞大,使用一台计算机显然无法存储这样的海量数据。另外从数据备份的角度考虑,也不能把所有数据只存储在一台计算机上,任何数据都需要存储在多台不同的机器上,既要把为用户提供的服务分散到不同的计算机上,也要考虑到一旦某台机器出现故障之后,在其他的机器上还有备份的数据,这样可以保证为用户提供持续不断的服务,并且确保用户的数据正确。既然任何数据都备份到了多台机器上,这里就必须要处理数据的一致性问题。 + +有一个水果店,他有一个用于收款的信用卡账户,初始余额为100元,“余额”这个数据就是存储在多台计算机上的。一个顾客前来购买了15元的苹果,并且通过信用卡付款。顾客付款之后,信用卡系统需要从顾客的账户里面扣除15元,在水果店的账户中添加15元。存储顾客账户“余额”的多台计算机都需要执行一致的操作,即:扣除15元,然后更新余额数据,保证更新结束之后,多台计算机上的“余额”是一致的。同样,存储水果店账户“余额”的多台计算机也需要相应的增加15元,并且保证添加完成之后,多台计算机上的“余额”是一致的,都是115元。假设有一台机器出错(暂时不讨论出错原因),余额变成了116元,而其他机器都是115元,这个时候水果店的“余额”这个数据在多台计算机上就不相同了,结果出错,即:数据一致性没有得到保证。对于水果店来说,他的信用卡账户里面到底是115元,还是116元呢?这就变成了一个至关重要的问题。 + +对于这一种类型的数据,其一致性就必须要获得保证,否则会带来灾难性的后果。 + +保证水果店信用卡账户的“余额”在所有机器上都是一样的,115元,把这个结果所呈现的状态称为**数据一致性**。而从顾客付款开始,到最终达到“一致性”这个结果的中间过程,称为**“共识算法”**。 + +**一致性描述的是结果的状态,而共识算法则是如何达成一致性的手段**。 + +上述付款的过程,就是一次典型的分布式系统的状态更新过程。下面我们来看看分布式系统的几个基础知识点。 + + +### 几个基础概念 + +**节点(Node):** 分布式系统是由一个一个的节点所组成。节点一般是指运行的一个软件的进程(process),如果一台物理计算机只运行了一个进程,那么这台计算机就代表了一个节点。相应的,如果运行了多个进程,就代表了多个节点。 + +**副本(Replica/Copy):** 虽然现代计算机硬件的故障率已经非常低,但并不是说完全没有故障,工程实践表明,计算机系统是肯定会出现故障的。所以对于分布式系统来说,任何数据都不能只保存在一台机器上,必须要保存在多台机器上,以保证数据不丢失以及对用户提供服务的连续性。相同的数据保存在多台机器上,每一份数据都称作是一份副本。一般来说,一个节点会对应一份数据副本。 + +**提案(Proposal):** 保存相同数据的不同节点都应该具有相同的初始状态,任何一个要改变这些状态的请求,称为一个提案(proposal)。比如前述例子中,顾客刷卡这个动作就产生了一个提案,这个提案请求改变两个账户的状态,顾客的账户减去15元,而水果店的账户增加15元。 + +**一致性(Consistency):** 对于存储在多个不同节点上的数据副本,都要保证是相同的。对于信用卡的网络来说,从收到这个提案(要求改变两个账户的状态)开始,共识算法就开始工作,共识算法需要确保,在所有相关的节点上,这两个账户的状态改变,要么全部同时成功,要么全部同时失败。无论成功还是失败,也无论是正确还是错误,只要所有相关节点的结果是相同的,就说达成了一致性。 + + + +### 分布式系统的常见问题 + +**节点故障:** 指的是组成分布式系统的节点出现“宕机”或者“假死”的现象。根据经验来看,每个节点都有可能发生故障,可能会由于硬件导致、由系统软件导致、或者由应用软件导致。 + +**通信异常:** 由于分布式系统依赖于通过网络来传递消息,而网络本身是不可靠的,随时都有可能发生数据丢失或者延迟的现象。网络光纤、路由器、DNS等等网络中的每一个环节都有可能导致通信故障,所以消息丢失和消息延迟在分布式系统中非常普遍。 + +**网络分区:** 俗称“脑裂”,由于通信异常的存在,导致部分节点之间的通信极大的延迟甚至中断,最终整个分布式系统之中的部分节点能够正常通信,而另外的部分则不能,在这种情况下,整个分布式系统被动的被划分为若干个小的子网络。在极端情况下,某些子网络能够独立完成原先需要整个分布式系统才能完成的工作。这个时候对数据一致性提出了严重的挑战。 + +**三态:** 这是分布式系统特有的现象,即:成功、失败、超时。在单机系统的时代,一个操作要么成功,要么失败。而在分布式系统中,由于通信异常的存在,有些操作,不知道是成功了,还是失败了,这种现象称为超时。主要有以下两种情况: +1. 消息没有成功的发送到接收方 +2. 消息成功的发送到了接收方,但是接收方发送回来的反馈消息丢失,发送方收不到反馈消息 + +在发生超时的情况时,发送方无法确定接收方的执行结果是成功还是失败 + + +### 分布式系统的几个基本原理 + +**FLP不可能原理:** +即使在网络通信可靠的情况下,对于可扩展的分布式系统,没有一个通用的共识算法能够解决任意场景下的一致性问题。 + +这个原理是说,在任意的分布式系统下,总是存在着某种异常,导致无法设计出通用的共识算法。当然这是极端的情况,在工程应用上,极端情况并不会总是出现,有些时候,多尝试几次,就成功了,哈哈 ~~~ + +**CAP原理:** +这个原理说明,对于一个分布式系统来说,无法同时满足以下三个特性: +- Consistency(C):一致性,即确保分布式系统的数据一致 +- Availability(A):可用性,即给用户提供的服务一直可用,对用户提出的每一次请求,都能够在有限的时间内返回有效的结果 +- Partition tolerance(P):分区容错性,在发生网络分区的时候,仍然能够对外提供满足一致性和可用性的服务 + +**ACID理论:** +这是一个著名的描述分布式数据库对数据一致性要求的原则,其主要特征是: +- Atomicity(A):每次操作都是原子性的,要么成功,要么不执行 +- Consistency(C):数据一致性是确定的,没有中间状态 +- Isolation(I):各种操作之间,彼此是隔离的,互不影响 +- Durability(D):状态的改变是持久的,不能丢失 + +**BASE理论:** +- Basically Available(BA):基本可用,在分布式系统出现故障的时候,允许损失部分可用性,比如返回时间长一些等 +- Soft state(S):软状态,也称为弱状态,指的是系统中的数据存在中间状态,并且这个中间状态不会影响到系统的整体可用性 +- Eventually Consistent(E):最终一致性,指的是系统中的所有数据副本,在经过一段时间的同步之后,最终能够达到一个一致的状态 + + +### 分布式系统、共识算法、区块链之间的关系 + +前面简述了分布式系统的基础知识,共识算法在分布式系统中的核心功能,就是确保在不同节点上的数据副本的一致性。 + +区块链,从本质上来说首先是一个分布式系统,所以在分布式系统上的各种问题,在区块链上也天然存在。在区块链的每一个节点上都保存有一份数据副本,对于全节点来说是一份完整的副本,对于轻节点来说是一部分数据的副本。**共识算法就是确保在区块链不同节点上的数据副本一致性的算法。** + +### 共识算法的要求 + +共识算法在达成一致性的过程中,需要满足如下要求: +- Termination:需要在有限的时间内达成一致性的结果 + + 对前面的例子(通过信用卡购买苹果)来说,需要在一个有限的时间内,比如说一分钟,达成一致性的结果,而不能是一个太长的时间。对于这个例子来说,如果需要一天的时间才能达成一致性的结果,那么这个系统就不具备实用价值 + +- Agreement:不同的机器最终完成的结果是相同的 + + 这个在前述例子中已经说明了 + +- Validity:不同机器最终达成的结果必须是某一个机器提出的提案 + + 这些不同的机器最终达成的结果,都是来自于顾客在水果店的刷卡动作,这个刷卡动作就产生了一个提案 + +与分布式系统相同,区块链的共识算法就是要在区块链的节点之间,达成区块链数据的一致性。 + + +### 小结 + +本文本着知其然亦知其所以然的出发点,着重介绍计算系统的演进,以及伴随而来的共识算法的演进过程。紧接着介绍了分布式系统的几个基本问题。对于具体的共识算法,我们会在后续的文章中逐步给出,敬请期待~~ diff --git a/consensus/consensus.byzantine.generals.md b/consensus/consensus.byzantine.generals.md new file mode 100644 index 0000000..3aaa486 --- /dev/null +++ b/consensus/consensus.byzantine.generals.md @@ -0,0 +1,105 @@ + + +# 共识算法之:拜占庭将军问题 + +### 拜占庭将军问题是什么 + +拜占庭将军问题是由Leslie Lamport在1982年提出,被称为The Byzantine Generals Problem或者Byzantine Failure。这个问题是Lamport在论文中抽象出来的著名例子,用来描述分布式系统一致性问题(Distributed Consensus)。其核心描述是在军队中可能有叛徒的情况下保证进攻一致,并由此引申到计算领域,发展成为一种容错理论。 + +一个简易的描述如下: + +拜占庭帝国,即中世纪的东罗马帝国,拥有巨大的财富,周围10个邻邦垂诞已久。但拜占庭高墙耸立,固若金汤,没有一个单独的邻邦能够成功入侵。任何单个邻邦入侵都会失败,同时也有可能自身被其他9个邻邦入侵。拜占庭帝国防御能力如此之强,至少要有十个邻邦中的一半以上同时进攻,才有可能攻破。然而,如果其中的一个或者几个邻邦事先答应好一起进攻,但实际过程出现背叛,那么入侵者可能都会被歼灭。于是每一方都小心行事,不敢轻易相信邻国。这就是拜占庭将军问题。 + +> 需要明确的是,在拜占庭将军问题中,并不去考虑通信兵是否会被截获或无法传达信息等问题,即消息传递的信道是可靠的。Lamport已经证明了在消息可能丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。所以在研究拜占庭将军问题的时候,我们已经假定了信道是可靠的,并在这个前提下,去做一致性和容错性相关研究。 + + +在这个问题里,各邻国最重要的事情是:所有将军如何能够达成共识去攻打拜占庭帝国。这些将军需要实现某一个统一的目标,一致进攻或者一致撤退,但是单独行动却又可能面临失败,所以必须达成共识,一致合作。由于叛徒的存在,将军们缺乏达成一致的有效途径。这里的“一致性”才是拜占庭将军问题需要探讨的内容,假如本来叛徒数量就已经多到了问题无解的地步,那么这个就是“反叛”的问题了。同时我们的目标是忠诚的将军能够达成一致,对于这些忠诚的将军来说,进攻或者撤退都是可以的,只要他们能够达成一致就可以。 + +但是光靠“一致”就可以解决问题了吗?仔细考虑一下,如果万事俱备,客观上每个忠诚的将军只要进攻了就一定能够胜利,但是却因为叛徒的存在他们都“一致的”没有进攻;反之,在条件不利的情况下,将军们不应该进攻,但是却因为叛徒的存在所有人都“一致的”进攻了。 + +从这个分析可以发现,只有“一致性”是不足以解决拜占庭将军问题的,还需要有一个“正确性”的要求。这个要求是值得思考的,因为如果客观来看或许会有“绝对正确的”判断,但是针对每一个将军,大家的判断或许都不相同,我们如何定义“正确”呢?或许可以简单地说,正确就是每个忠诚的将军都正确的表达了自己的意思,不会因为叛徒让别的将军认为忠诚的将军是叛徒而不采用他传达的消息。 + +在这些将军里面,如果出现了叛徒,情况就会变得异常复杂,可能会出现如下的问题: +* 叛徒可能欺骗某些将军自己将采取进攻行动 +* 叛徒可能怂恿其他将军行动 +* 叛徒可能迷惑其他将军,使他们接受不一致的信息,从而感到迷惑 + +至此,我们将拜占庭将军问题简化成了,所有忠诚的将军都能够让别的将军接收到自己的真实意图,并最终一致行动;而形式化的描述就是,“一致性”与“正确性”。 + +如果将问题发散一下,可以发现针对一致性和正确性的算法并不要求命令必须是“进攻/撤退”,而可以是“发送消息A/发送消息b”等等,这意味着拜占庭将军问题算法可以为多种分布式系统提供启发,比如火车票售票系统等。 + +这个问题说到底是一个关于一致性和正确性的算法问题,这个算法针对的是忠诚的将军,因为叛徒可以做出任何超出约定的判断。我们就是要在有叛徒的干扰下,找到一个抗干扰的算法。要解决这个算法问题,我们需要将形式化要求具体化。 + + +### 问题分析 + +我们来分析一下几种具体的情况: + +先看在没有叛徒情况下。假如一个将军A提一个进攻提议(如:后天下午2点进攻,你同意吗?),由通信兵分别告诉其他的将军,如果非常幸运,他收到了其他至少5位将军的同意,发起进攻。如果不幸,其他的将军也在此时发出不同的进攻提议(如:明天上午8点进攻,你同意吗?),甚至提出相反的提议(如:今天半夜11点撤退)。由于时间上的差异,不同的将军收到(并认可)的进攻提议可能是不一样的,这时可能出现A提议有2个支持者,B提议有5个支持者,C提议有2个支持者等等。 + +如果这些将军中有叛徒的话,一个叛徒可能会向不同的将军发出不同的进攻提议(通知A明天上午7点进攻,通知B明天下午2点进攻等等),一个叛徒也可能同意多个进攻提议(即同意下午1点进攻,又同意上午9点进攻)。在这种情况下,整体上将军们的进攻计划就彻底乱套了。叛徒发送前后不一致的进攻提议,被称为“拜占庭错误”,而能够处理拜占庭错误的这种容错性称为“Byzantine Fault Tolerance”,简称为BFT。 + + +### 最简模型 + +假设只有3个人 A、B、C,假设三人中其中一个是叛徒。当A发出进攻命令时,B如果是叛徒,他可能告诉C,他收到的是“撤退”的命令。这时C收到一个“进攻”,一个“撤退“,于是C被信息迷惑,而无所适从。 + +如果A是叛徒。他告诉B“进攻”,告诉C“撤退”。当C告诉B,他收到“撤退”命令时,B由于收到了A的“进攻”的命令,而无法与C保持一致。 + +正由于上述原因,在只有三个角色的系统中,只要有一个是叛徒,即叛徒数等于1/3,拜占庭问题便不可解。 + +如果叛徒的数量大于或等于1/3,拜占庭问题不可解。 + +Lamport在论文中提出了口头信息方案和书面协议两个方案。 + +### 解决方案一:用口头信息 + +所谓口头信息,就是将军们使用信使传递口头信息,要满足以下三个条件: + +* 被发送的消息能够被信使正确传递 +* 接受者知道消息是哪个将军发的 +* 能够知道谁没有发送消息 + +也就是说,信道可信,消息来源可知。口头协议的算法很简单,如果其中一个将军,比如A +发布消息出去,其他9个将军都接收到将军A的消息,然后这9个将军也分别转告给其他的将军,每个将军都是信息的转达者。一轮下来,每个将军手上都会有10个信息(进攻或者撤退)。如果有叛徒的话,那信息可能包含有进攻或者不进攻这样的不一致消息。每个将军相当于手里有一本消息的账本,该怎么决策呢?如果有一半以上的人说进攻,那么采取进攻行动就是能成功的,所以这时即便有叛徒,只要听大部分人的,少数服从多数来行动即是有利的。 + +这种口头协议的算法也存在明显的缺点:口头协议并不会告知消息的上一个来源是谁,也就是消息不可追根溯源,出现信息不一致也很难找到叛徒在哪。 + +### 解决方案二:用书面信息 + +一种不同于使用口信来传递信息的方法,是使用书信,并且在书信上都要签上将军们的印章,相比于口头协定,又多了两个条件: + +* 将军使用印章对书信签名,这个签名就确定了将军的身份,不可伪造,篡改签名可被发现 +* 收到书信的任何其他将军都可以验证签名的有效性 + +书面信息的本质就是引入了“签名系统”,这使得所有消息都可追本溯源。只要采用了书面信息,忠诚的将军就可以达到一致。现在这种方式下,将军们按照以下方式发送消息: + +* 每位将军分别给其他将军发送书信,并在书信上附上自己的签名 +* 其他将军收到书信后,附上自己的签名后再发给所有其他将军 +* 每位将军根据自己收到的书信进行决断 + +书面信息貌似完美地解决了拜占庭将军问题,但是不得不说实际上的解决是建立在诸多限制条件下的。在现实的分布式系统中,我们可能会遇到各种各样的问题。例如: + +* 没有考虑信使传递消息的时延问题 +* 真正可信的签名体系很难实现,也很难避免签名造假 +* 将军们的印章是国王颁发的,难以褪去中心化机构的影响 + +另外,如果每个将军都向其他将军派遣信使表达自己的观点,那么一轮信息交流需要90次的信使往来,而且每个将军的观点都可能不一致。在异步通信模式下,这种模式几乎很难达成一致。而且让所有将军都相信中心化的国王签发的印章的真实性,实际上也违反了整个问题的前提,那就是将军们互相不信任,即便是有国王的存在。 + +### 解决思路 + +拜占庭将军问题之所以难解,一个重要的原因就是在任意时间,系统中可能会存在多个提案,也就是问题描述中的每个将军都可以提出自己的进攻或者撤退意见。这样一来,很难在一个时刻对结果进行一致性确认。中本聪创新性地引入PoW共识算法,解决了两个困难。 + +* 限制一段时间内提案的个数,只有拥有对应权限的节点(将军)可以发起提案。在比特币里,是通过随机哈希计算分配权限的,谁第一个计算出对应的答案,谁才有权限发起提案。这种方案就极大的减少了在某一个时间点的提案数量,整体上减少了提案太多的混乱状况 +* 区块链技术使用非对称加密算法对节点间的消息传递提供签名技术支持,每个节点(将军)都有属于自己的秘钥(公钥私钥),唯一标识节点身份。使用非对称加密算法传递消息,能够保证消息传递的私密性,而且消息签名不可抵赖,不可篡改 + * 使用公钥加密的数据,使用公钥对应的私钥进行解密;使用私钥进行签名的消息,只需要使用私钥对应的公钥验证签名即可。比如,A将军想要给B将军发送消息,那么只需要使用B将军的公钥加密消息,B将军收到消息后使用自己的私钥解密消息即可。而如果A将军想申明自己的身份,只需要将消息使用自己的私钥进行签名即可,B将军收到消息后就可以使用A将军的公钥验证消息的来源。这样就将一个不信任的网络变成了信任网络 + +在区块链这样的分布式网络中,我们还是以将军为例: + +* 每位将军都保留一份历史消息账本 +* 因为每份消息都是进行过签名的,所以如果有背叛的将军,我们很容易就能找出来; +在一轮共识的流程里,即便有消息不一致,但是只要背叛将军的个数少于1/3,这一轮共识就能达成 + +## 小结 + +到了这里,我们可以很清楚地知道,区块链和拜占庭将军问题的共性所在,都是决定由谁发起消息(提案);然后在消息的传递过程中都需要明确是谁在传递消息;以及如何在分布式系统中达成一致的问题。 diff --git a/consensus/consensus.paxos.md b/consensus/consensus.paxos.md new file mode 100644 index 0000000..eb08aea --- /dev/null +++ b/consensus/consensus.paxos.md @@ -0,0 +1,198 @@ + +# 共识算法之:Paxos + + +## 介绍 + +在2006年Google的经典论文“The Chubby lock service for loosely-coupled distributed systems”中有一句话,”Indeed, All working protocols for asynchronous consensus we have so far encountered have Paxos at their core"(事实上,到目前为止我们遇到的解决异步共识问题的实用算法,其核心都是Paxos)。 + +Google大牛的话,直接指出了Paxos的江湖地位,那么Paxos到底是那一路武林门派? + + +## Paxos的诞生 + +Leslie Lamport在1989年写出第一篇论文《The Part-Time Parliament》的时候,使用了一个虚拟的小岛Paxos,岛上的人们通过议会表决的方式来决定具体事务,Lamport就通过这种方式来解释这个算法,群众纷纷表示难以理解,大神表示你们一点儿幽默感也没有,拒绝修改自己的论文,导致这篇论文也没有能够发表。随着分布式系统的发展,有一部分人读懂了这篇论文,直到1998年这篇论文才得以最终发表。随后Lamport也做出了让步,后续又陆续发表了《Paxos Made Simple》和《Fast Paxos》两篇论文。大家也就习惯性的把这套算法成为Paxos算法。 + +Lamport在《Paxos Made Simple》中写道:The Paxos algorithm, when presented in plain English, is very simple. 大神眼里的very simple在普通人这里变成了异常的晦涩难懂,尤其是要使用程序员严谨的思维将所有细节理清的时候,那更是云山雾罩。 + +那么,Paxos究竟在解决什么问题?算法的核心思想是什么? + + +## Paxos 是什么 + +分布式系统中的跨节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing)。共享内存的方式,只适合于不同的节点共存于同一台物理机器的时候,当跨越物理机器的时候,就只能使用消息传递了。 + +基于消息传递模型的分布式系统,不可避免的会发生以下异常: +- 进程可能会慢、被杀死或者重启; +- 消息可能会延迟、丢失、重复; + +注:在基础Paxos场景中,先不考虑可能出现消息篡改(即拜占庭错误)的情况; + +Paxos算法解决的问题,就是在一个可能发生上述异常的分布式系统中,如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。注意,这里的“某个值”,可以是某一个具体的数值,比如,1,2,3,4,5。也可以是某一种具体的操作,这个操作可以序列化成“一个值”,只要这个“值”确定了,也就等于这个操作确定了。 + +** 维基百科的定义 ** + +Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos可以说是一个民主选举的算法——大多数节点的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的节点同意。取值一旦确定将不再更改,并且可以被获取到(不可变性,可读取性)。 + +简而言之,这个就有点像我们的人民代表大会制度,每一个与会代表都可以提出自己的提案,只要能够获得超过半数的代表同意,这个提案就可以获得通过。 + +我们现在知道了Paxos是干什么的,下面继续来看,他是怎么干的。 + +## Paxos算法 + +#### 角色说明 + +Paxos把这个过程中的所有参与者划分成了几种角色,分别如下: +- Proposer:提议者,提出议案(同时存在一个或者多个,他们各自发出提案) +- Acceptor:接受者,收到议案后选择是否接受 +- Learner:最终决策学习者,只学习正确的决议 +- Client:产生议题者,发起新的请求 + + +![](./imgs/paxos.roles.jpeg) + +上面这一幅图表示出了各个角色之间的关系。这4种角色中最主要的是Proposer和Acceptor。Proposer就像Client的代理人,由Proposer拿着Client的议题去向Acceptor提议,让Acceptor来做出决策。主要的交互过程在Proposer和Acceptor之间。 + +这幅图表示的是角色之间的逻辑关系,每一种角色就代表了一种节点类型。在物理部署环节,可以把每一种角色都部署在一台物理机器上,也可以组合任何两种或者多种角色在一台物理机器上,甚至于,把这四种角色都部署在同一台物理机器上也是可以的。 + +#### 算法细节 + +Paxos算法描述非常的简单,就只有两步,决议的提出(Prepare)与批准(Accept)。就这么一句话,你懂了么?是不是云里雾里的。 + +###### Prepare阶段 + +proposer选择一个提案编号n并将Prepare请求发送给acceptors中的一个多数派(即超过半数);acceptor收到Prepare消息后,如果提案的编号大于它已经回复的所有Prepare消息,则acceptor将自己上次接受的提案回复给proposer,并承诺不再回复小于n的提案; + +![](./imgs/paxos.prepare.jpeg) + +上图是一个proposer和5个acceptor之间的交互,对2种不同的情况做了处理。 + +###### 批准阶段 + +当一个proposer收到了多数acceptors对Prepare的回复后,就进入批准阶段。它要向回复Prepare请求的acceptors发送accept请求,包括编号n和value; + +在不违背自己向其他proposer的承诺的前提下,acceptor收到accept请求后即接受这个请求。 + +![](./imgs/paxos.accept.jpeg) + +可以看出,Proposer与Acceptor之间的交互主要有4类消息通信,这4类消息对应于Paxos算法的两个阶段4个过程。用2轮RPC来确定一个值。上面的图解都只有一个Proposer,但是实际肯定是有多个Proposer发出请求,所以在每个过程中都会有些特殊情况处理,这也是为了达成一致性所做的事情。 + +如果在整个过程中没有其他Proposer来竞争,那么这个操作的结果就是确定无异议的。但是如果有其他Proposer来竞争的话,情况就不一样了。 + + +上面的描述就是比较正统的解释Paxos算法的说明了,怎么样,你看懂了吗?作为初学者第一次看到这样的描述的时候,一般都在云里游荡呢,反正我第一次看到的时候是这样。下面,我们还是用生活化的例子来说明一下吧。 + + +#### 四个人约饭局的问题 + +我们先来看一个最简单的模型,只有一个Proposer的情况。 + +这里有四个人要约饭局,就用A,B,C,D来代表着四个人吧,他们要确定的是去吃什么的问题,他们彼此之间还是只能通过短信沟通。大家都很谦让,都说你们说吧,看看去吃什么,在这种情况下,每个人都没有做出决定。 + +初始状态是这样的: + +| | A | B | C | D | +| - | :-: |:-: | :-: | :-: | +| 编号 | 0 | 0 | 0 | 0 | +| 选择 | NULL | NULL | NULL | NULL | + +这种沉默也不能无限期下去吧,终于A站出来,发出了第一个提议,我们去吃海底捞吧,并且给另外三个发了短信。这个时候,A就是Proposer,B,C,D三个人的角色就成了Acceptor。这三个人都正确的收到了短信,而且爽快的回复了短信说,你决定就好了,反正我这里还没有做出选择。 + +这个时候,状态是这样的: + +| | A(Proposer) | B(Acceptor) | C(Acceptor) | D(Acceptor) | +| - | :-: |:-: | :-: | :-: | +| 编号 | | 1 | 1 | 1 | +| 选择 | | NULL | NULL | NULL | + +A收到了B,C和D回复的短信,这是A的状态是这样的。 + +| | A(Proposer) | +| - | :-: | +| 编号 | 1 | +| 选择 | NULL | + +这时候,由于A收到了3个人的确认短信,已经超过了Acceptors(三个人)的半数,也就意味着A的提案被接受了。所以A给三个人再次发出确认短信,确定去海底捞吃饭,B,C,D收到后,都同意了这个提议。由于超过了半数Acceptors的批准,这次去哪儿吃饭的事情就定下来了。这时候最新的状态是这样的。 + +| | A(Proposer) | B(Acceptor) | C(Acceptor) | D(Acceptor) | +| - | :-: |:-: | :-: | :-: | +| 编号 | | 1 | 1 | 1 | +| 选择 | 海底捞 | 海底捞 | 海底捞 | 海底捞 | + +在大家都确定好了之后,又一个冒失鬼E突然闯了进来,给四个人都发了短信问,去哪儿吃饭,捎上我呀。B,C,D都给他回短信,去海底捞吃饭,你来吧,负责买单就好。这个情况下,E就是Learner角色。 + +| | E(Learner) | +| - | :-: | +| 选择 | 海底捞 | + +至此,约饭局结束。这里只是一个最简单的模型,只有一个Proposer提出提案,没有人竞争,所有大家都直接接受这个唯一的提案者提出的建议就好了。 + +#### 七个人约饭局的问题 + +下面,我们看一个稍微复杂一点的例子,有2个Proposers和5个Acceptors. 这种情况下,就已经不失一般性了,更多的Proposer和Acceptors的情况下,很可能就是交互的轮次会更多而已。 + +在这里,我们就直接假定,A和B是Proposer,其余的C,D,E,F,G是Acceptor,这里是所有角色的初始状态,所有的Acceptors都没有响应过任何编号的提案,所以编号都是0,且选择都是NULL + + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 0 | 0 | 0 | 0 | 0 | +| 选择 | NULL | NULL | NULL | NULL | NULL | + +这时候,A发起了第一轮建议,给所有的Acceptors发短信说,我们去吃海底捞吧,由于异常,只有两个Acceptors收到了这个Prepare(1, 海底捞)的短信。由于这两个Acceptors也是没有任何主见的人,所以他们就爽快的同意了A的建议,并且给A回复了承诺短信promise(1, NULL) + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 0 | 1 | 0 | 0 | 1 | +| 承诺 | 0 | 海底捞 | 0 | 0 | 海底捞 | +| 选择 | NULL | NULL | NULL | NULL | NULL | + +> Paxos协议的第一阶段,就是proposer发起提案,每一个提案都需要包含一个提案的唯一编号。对于acceptor来说,如果他还没有承诺过任何提案,那么就必须对收到的第一个提案做出承诺,即:把这个提案作为备选的可接受提案。做出承诺的方式就是给proposer做出承诺回复,回复的信息是proposal的提案编号和空值(NULL),NULL表示自己还没有承诺过任何提案 + +由于A只收到了D和G两个人的承诺,未能超过半数,所以无法对所有人发起确认消息。 + +> Proposer必须要收到超过半数以上Acceptors的承诺,才能发起accept请求 + +随后,B发起第二轮建议,给所有人发短信说,我们去吃披萨吧。Prepare(2,披萨),由于异常原因,只有C,D,E,F四个人处理了他的消息,C,E,F由于没有做出过任何承诺,所以给B回复了承诺短信promise(2, NULL),B由于已经给A做出过承诺,承诺了海底捞,所以给B回复了承诺promise(2, 海底捞) + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 2 | 2 | 2 | 2 | 1 | +| 承诺 | 披萨 | 海底捞 | 披萨 | 披萨 | 海底捞 | +| 选择 | NULL | NULL | NULL | NULL | NULL | + +> 如果Acceptor已经向Proposer承诺过某个编号的提案,在收到了新的提案时,会先去比较提案编号。如果新的提案编号小于或者等于前面承诺过的提案编号,则会忽略这个新的提案。如果新的提案编号大于曾经承诺过的提案编号,那就把曾经承诺过的提案连同新的提案编号回复给这个Proposer,并且向这个新的Proposer承诺,不会再响应任何低于这个新提案编号的其它提案 + +B收到了上述四个Acceptors的承诺回复,由于C,E,F三个人回复都相同,同意他说的去吃披萨,超过了半数,所以B给所有人发送了短信,通知大家都去吃披萨,Accept(2, 披萨)。由于异常原因,只有C,D收到了这个短信,并且接受了这个建议。 + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 2 | 2 | 2 | 2 | 1 | +| 承诺 | 披萨 | 海底捞 | 披萨 | 披萨 | 海底捞 | +| 选择 | 披萨 | 披萨 | NULL | NULL | NULL | + +> Proposer必须在收到了超过半数的Acceptors的承诺之后,才可以发起accept请求,要求所有的Acceptors批准这个提案。 + +这个时候,A发起了第三轮建议,给所有Acceptors发送短信,还是建议去吃海底捞。Prepare(3,海底捞),这时候C,E,F,G都收到了这个建议。C回复说,我已经批准了去吃披萨,Promise(3,披萨)。E,F则发现,他们之前已经承诺了去吃披萨,于是给A回复说,行,我们还是去吃披萨吧,Promise(3,披萨)。G则发现,他之前的承诺是去吃海底捞,所以还是回复说吃海底捞。Promise(3,海底捞)。 + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 3 | 2 | 3 | 3 | 3 | +| 承诺 | 披萨 | 海底捞 | 披萨 | 披萨 | 海底捞 | +| 选择 | 披萨 | 披萨 | NULL | NULL | NULL | + +> 对于Acceptor来说,在收到Prepare消息的时候,如果它已经批准了(Accept)某项提案(Proposal),则必须把这个已经批准的提案回复给Proposer。如果他已经响应过之前的某项提案,则需要把之前响应过的最大编号的提案回复给Proposer。 + +A收到了这四个回复承诺,有三个(超过半数了)都说去吃披萨,A一看,只好少数服从多数,就给所有人回复短信说,那好吧,听你们的就去吃披萨好了,Accept(3,披萨)。 + +| | C(Acceptor) | D(Acceptor) | E(Acceptor) | F(Acceptor) | G(Acceptor) | +| - | :-: | :-: | :-: | :-: | :-: | +| 编号 | 3 | 2 | 3 | 3 | 3 | +| 承诺 | 披萨 | 海底捞 | 披萨 | 披萨 | 海底捞 | +| 选择 | 披萨 | 披萨 | 披萨 | 披萨 | 披萨 | + +> proposer必须要选择超过半数Acceptors承诺的值发起accept请求。至此,所有的acceptors达成共识。 + +## 小结: + +我们通过一个现实生活中约饭局的例子,来形象的描述Paxos算法。在这个例子中我们也可以看到,在确定一个值的过程中,就是在应用少数服从多数的法则。然而,核心算法与实际的工程还有比较大的距离,在实际的项目中,还有诸多的细节需要仔细思考。 diff --git a/consensus/consensus.raft.md b/consensus/consensus.raft.md new file mode 100644 index 0000000..a85c696 --- /dev/null +++ b/consensus/consensus.raft.md @@ -0,0 +1,107 @@ + + +# 共识算法之:Raft + +## 简介 + +Paxos算法是分布式系统领域最重要的一致性算法,同时也是公认的极为艰深难懂的算法。为了解决这个晦涩难懂的问题,斯坦福大学的Diego Ongaro、John Ousterhout教授以容易理解(Understandability)为目标设计了这个新的一致性算法:Raft,并在2013年发布了论文:《In Search of an Understandable Consensus Algorithm》。为了验证这个容易理解的特性,他们分别在斯坦福大学和加州大学伯克利分校的分布式计算课程上,使用了Raft和Paxos两种算法,采用视频教学的方式来传授给学生,之后采用小测验的方式来验证。结果表明Raft比Paxos容易理解很多。 + +Raft和Paxos一样只要保证n/2+1节点(即超过半数节点)正常工作就能够提供服务。在设计层面,Raft把算法流程分为三个子问题:领导选举(Leader election)、日志复制(Log replication)、安全性(Safety)。 Raft开始时在集群中选举出Leader负责日志复制的管理,Leader接受来自客户端的事务请求(日志),并将它们复制给集群的其他节点,然后负责通知集群中其他节点提交日志,Leader负责保证其他节点与他的日志同步,当Leader宕机后集群其他节点会发起选举选出新的Leader。 + + +## Raft算法详解 + +### 角色 + +Raft把集群中的节点分为三种状态:Leader、Follower和Candidate,在任一时间,每一个节点都只能处于这三种状态中的一种,相应的我们把每一种状态称为一种角色,每种角色的任务也是不一样的: + +* Leader(领导者):负责日志的同步管理,处理来自客户端的请求,与Follower保持着heartBeat的联系 +* Follower(追随者):刚启动时所有节点都处于Follower状态,响应Leader的日志同步请求,响应Candidate的请求,把Client请求到Follower的事务转发给Leader +* Candidate(候选者):发起选举投票,Raft刚启动时由一个节点从Follower转为Candidate发起选举,选举出Leader后从Candidate转为Leader状态 + +![](imgs/raft.state.switch.png) + +Raft节点的状态转换图 + +Raft刚启动的时候,所有节点初始状态都是Follower,每一个节点都设置了一个随机的超时时间,这个时间一般是在150毫秒到300毫秒之间。超时时间内如果没有收到Leader的请求则转换为Candidate角色并发起Leader选举。如果Candidate收到了多数节点的选票则转换为Leader;如果在发起选举期间发现已经有Leader了,或者收到更高任期的请求则转换为Follower。Leader在收到更高任期的请求后转换为Follower。 + +### 任期(Term) + +Raft把时间切割为任意长度的任期(Term),每个Term都是一个连续递增的编号,采用连续的整数来表示。每个任期都由一次选举开始,每一轮选举都是一个Term任期,若选举失败则这个任期内没有Leader。如果选举出了Leader则这个任期内由Leader负责集群状态管理,在一个Term中只能产生一个Leader。 + +![](imgs/raft.terms.png) + +Raft中Term图示 + + +先描述下Term的变化流程:Raft开始时所有Follower的Term为1,其中一个Follower的超时时间到期后转换为Candidate,Term加1(这时Term变为2),然后开始选举,这时候有几种情况会使Term发生改变: + +* 如果当前Term为2的任期内没有选举出Leader或出现异常,则Term递增,开始新一任期选举 +* 当这轮Term为2的周期选举出Leader后,过一段时间之后Leader挂掉了,然后其他Follower转为Candidate,Term递增,开始新一任期Leader选举 +* 当Leader或Candidate发现自己的Term比别的Follower小,Leader或Candidate将转为Follower,Term递增 +* 当Follower的Term比别的Term小,Follower也将更新Term保持与其他Follower一致 + +每次Term的递增都将发生新一轮的选举,Raft保证一个Term任期内只有一个Leader,在Raft正常运转中所有的节点的Term都是一致的,如果节点不发生故障一个Term会一直保持下去,当某节点收到的请求中Term比当前Term小时则拒绝该请求。 +   + +### 领导选举(Leader Election) + +有两种情况会触发选举: + +* Raft初次启动时,不存在Leader,这时候会触发Leader选举 +* Follower在自己的超时时间内没有接收到Leader的heartBeat,触发election timeout,从而Follower的角色切换成Candidate,Candidate会发起选举 + +当一个节点的状态由Follower转换为Candidate后,立刻就触发了Leader选举,Candidate节点首先会把Term号加1,然后向所有其他节点发起Leader Request Vote请求,这时候有以下几种可能的情况发生: + +* 该Request Vote请求接收到n/2+1个节点(过半数)的投票,从Candidate转为Leader,然后立刻向其他节点发送heartBeat以保持Leader的正常运转。必须要收到过半数的投票,保证了在一个Term周期内,只有一个Leader存在 +* 在此期间如果收到其他节点发送过来的Append Entries请求,如该节点的Term号大于自己的Term号,则当前节点转为Follower,否则拒绝该请求,保持自己为Candidate +* 收到其他节点的Request Vote请求,如果请求中的Term号大于Candidate当前的Term号,则认为自身的Term已经过期了,自身转换为Follower,并且给其他节点投票。如果其他节点的Term号小于自身的Term号,则拒绝该请求并保持自身的Candidate角色 +* 在此期间如果没有收到足够多的投票,然后会发生超时,则Term递增,发起新一轮选举 + +当一个Candidate转换成为Leader之后,需要周期性的发送心跳包(如果有正常的RPC请求情况下则可以不发心跳)保持自己Leader的角色(避免集群中其他节点认为没有Leader而开始选举)。 + +Follower在收到Leader或者Candidate的RPC请求的情况下一直保持Follower状态。而当一段时间内(election timeout)没有收到请求则认为没有Leader节点而再次触发选举流程。  + +### 分裂选举 + +在一个Term期间每个节点只能投票一次,所以当有多个Candidate存在时就会出现每个Candidate发起的选举都存在接收到的投票数都不过半的问题,这时每个Candidate都将Term递增、重启定时器并重新发起选举,由于每个节点中定时器的时间都是随机的,所以就不会多次存在有多个Candidate同时发起投票的问题。 + +每一个Follower的超时时间设置,都是采用随机时间的方式,时间周期在150毫秒到300毫秒之间。如果所有Follower的超时时间设置都一样,那么Follower都会在几乎同一时间切换成Candidate,这些Candidate又会同时都发起Leader选举,这种情况下,几乎不会有Candidate能够收到超过一半的节点给予的投票,从而进入下一轮的选举。随机超时时间虽然不能绝对确保时间都不相同,但是产生出相同的随机时间毕竟是非常小概率的事件,而且这种情况也会很快就被下一轮随机时间所解决。 + +两个或者多个Follower同时转换为Candidate并且发起选举的时候,如果其中有一个Candidate能够收到半数以上的投票,那么自然就会成为Leader并且发送heartBeat信息,开始新一轮Term周期。如果没有任何一个Candidate获得超过半数以上的投票,那么在这一段时间内,每一个节点又开始了新一轮超时时间周期,在这一轮超时时间周期内,由于没有Leader,所有的节点都不可能收到heartBeat消息。随后,第一个到达超时时间的节点会发起新一轮的Leader选举,从而打破这种平衡。 + +### 日志复制(Log Replication) + +一旦Leader被选举出来后,Leader就开始为集群服务:处理所有的客户端请求并将数据复制到所有节点。 + +一旦日志被“安全”的复制,那么Leader将这个日志应用到自己的状态机并响应客户端。 + +如果有节点异常或网络异常,Leader会一直重试直到所有日志都会正确复制到所有节点(日志不允许有空洞,所以每个节点上的日志都是连续的,不能有因为失败引起的空洞)。 + +![raft.log.structure](imgs/raft.log.structure.png) + +日志组织形式如上图,每个日志条目中包含可执行的指令、和日志被创建时的任期号,日志条目也包含了自己在日志中的位置,即index。一旦一个日志条目存在于大多数节点,那么该日志条目是committed的。 + +Raft算法保证所有committed的日志都是持久化的。日志需要在大多数节点上持久化之后再响应给客户端,这意味着每个Follower节点收到Append Entry请求后需要持久化到日志之后再响应给Leader,且最终会被所有的状态机执行。 + +Raft算法保证了以下特性: + +* 如果两个日志条目有相同的index和term,那么他们存储了相同的指令(即index和term相同,那么肯定是同一条指令,就是同一个日志条目) +* 如果不同的日志中有两个日志条目,他们的index和term相同,那么这个条目之前的所有日志都相同 + +两条规则合并起来的含义:两个日志LogA、LogB,如果LogA[i].index=LogB[i].index且LogA[i].term=LogB[i].term,那么LogA[i]=LogB[i],且对于任何k < i的日志条目,LogA[k]=LogB[k]都成立。 + +### 安全性(Safety) + +安全性是用于保证每个节点都执行相同序列的安全机制,如当某个Follower在当前Leader commit Log时变得不可用了,稍后可能该Follower又会被选举为Leader,这时新Leader可能会用新的Log覆盖先前已committed的Log,这就是导致节点执行不同序列;Safety就是用于保证选举出来的Leader一定包含先前committed Log的机制。 + +* 选举安全性(Election Safety) +* 每个Term只能选举出一个Leader +* Leader完整性(Leader Completeness) + +这里所说的完整性是指Leader日志的完整性,当Log在Term1被Commit后,那么以后Term2、Term3、Term4…等的Leader必须包含该Log。Raft在选举阶段就使用Term的判断用于保证完整性:当请求投票的该Candidate的Term较大或Term相同Index更大则投票,否则拒绝该请求。 +   +   +## 小结 + +Raft以容易理解为设计目标,通过前面的描述可以感受到,相比于Paxos,Raft的确非常容易理解。在工程上,也已经有了众多的项目在实际上使用Raft作为其一致性算法,比如著名的高可用分布式的键值数据存储系统etcd(https://github.com/coreos/etcd) diff --git a/consensus/eos.dpos.md b/consensus/eos.dpos.md new file mode 100644 index 0000000..ae01027 --- /dev/null +++ b/consensus/eos.dpos.md @@ -0,0 +1,405 @@ + +# EOS DPoS 共识算法 + +EOS的DPoS共识算法,总体上来说,是由EOS的持币人,投票给想要作为记账的节点,然后每隔63(21 * 3)秒统计一次每个节点获得的选票,获得选票数最多的21个节点成为超级节点,参与下一轮的记账,得票数排在后面的100个节点作为备选节点。 + +每一个EOS代币,最多可以投票给30个节点,但是不能重复投票,也就是说,一个币给一个节点只能投一票。 + +下面,让我们首先来看看投票的过程。 + + +### 注册节点 + +注册节点,意思就是说,如果一个节点想要成为记账节点,获得EOS持有者的投票,必须先把自己注册到候选节点里面去。这个就像是美国的总统选举,想当总统是吧,要先想全世界的人宣布,自己参选了。 + +注册自己的时候,首先会做合规检查,url的长度必须小于512字节。然后,eosio有一个公共的public key,这个也是不能注册为记账节点的。对于还没有注册过的节点,直接添加到候选节点队列中;如果一个账户已经注册过了,再注册一次的话,啥事儿都不会做的(难道还是多吃多占不成?), + + + +```c++ +// source file: ./eos/contracts/eosio.system/voting.cpp +// producer : 这个是候选者账户的的名字 +// producer_key : 这个是候选者账户的 public key +void system_contract::regproducer(const account_name producer, + const eosio::public_key& producer_key, + const std::string& url, + uint16_t location ) { + // 检查url长度 + eosio_assert( url.size() < 512, "url too long" ); + // eosio有一个公共的public key,这个是不能注册的 + eosio_assert( producer_key != eosio::public_key(), "public key should not be the default value" ); + require_auth( producer ); + + auto prod = _producers.find( producer ); + + if ( prod != _producers.end() ) { + if( producer_key != prod->producer_key ) { + // 确保这个账户没有被注册过。如果有一个同名的,则改之 + _producers.modify( prod, producer, [&]( producer_info& info ){ + info.producer_key = producer_key; + info.is_active = true; + info.url = url; + info.location = location; + }); + } + } else { + // 添加到竞选队列,恭喜,现在有被选举权了 + _producers.emplace( producer, [&]( producer_info& info ){ + info.owner = producer; + info.total_votes = 0; + info.producer_key = producer_key; + info.is_active = true; + info.url = url; + info.location = location; + }); + } +} + +// 记账太累了,我不干了,休息,休息,休息一会儿/ +// 告诉所有人,我退出竞选了 +void system_contract::unregprod(const account_name producer) { + require_auth( producer ); + + const auto& prod = _producers.get( producer, "producer not found" ); + + _producers.modify( prod, 0, [&]( producer_info& info ){ + // 标记为非活动状态 + info.deactivate(); + }); +} +``` + + +### 更新记账节点队列 + +每一轮记账结束之后,都会根据在这一段时间内的投票结果更新记账节点队列,会把得票数最高的21个节点作为超级节点选择,进入下一轮的记账工作。 + +这个update_elected_producers函数是被eosio.system::onblock函数调用了,这个onblock函数是向记账节点付账的时候调用了,在这个函数里面,我们可以看到,大于120秒的时候,会调用update_elected_producers函数还更新超级节点列表 + +```c++ +// source file: ./eos/contracts/eosio.system/voting.cpp +void system_contract::update_elected_producers( block_timestamp block_time ) { + // 保当前时间保存下来 + _gstate.last_producer_schedule_update = block_time; + + auto idx = _producers.get_index(); + + // 产生新的超级节点,这在这里限定了最多只有21个 + std::vector< std::pair > top_producers; + top_producers.reserve(21); + + // 从候选节点列表中获取票数最多的21个节点(候选节点列表是排好序的) + for ( auto it = idx.cbegin(); + it != idx.cend() && top_producers.size() < 21 && 0 < it->total_votes && it->active(); + ++it ) { + top_producers.emplace_back( std::pair({{it->owner, it->producer_key}, it->location}) ); + } + + if ( top_producers.size() < _gstate.last_producer_schedule_size ) { + return; + } + + /// 把选出的21个节点按照名字排序 + std::sort( top_producers.begin(), top_producers.end() ); + + std::vector producers; + + producers.reserve(top_producers.size()); + for( const auto& item : top_producers ) + producers.push_back(item.first); + + bytes packed_schedule = pack(producers); + + if( set_proposed_producers( packed_schedule.data(), packed_schedule.size() ) >= 0 ) { + _gstate.last_producer_schedule_size = static_cast( top_producers.size() ); + } +} + + +// source file: ./eos/contracts/eosio.system/producer_pay.cpp +void system_contract::onblock( block_timestamp timestamp, account_name producer ) { + // 当时间差大于120秒的时候,更新超级节点列表 + if( timestamp.slot - _gstate.last_producer_schedule_update.slot > 120 ) { + update_elected_producers( timestamp ); + } +} +``` + + +### 注册成为投票代理 + +对于每一个账户来说,投票方式有两种,一种是自己亲自出马,直接给候选节点投票。还有一种是自己不想直接参与投票,那可以选择一个代理人来帮自己投票,让他来代替你进行投票。投票代理对那些想要投票,但无法了解全部节点候选人的用户来说是非常有用的。 + +如果一个账户选择了另外一个账户作为投票代理的话,则要把自己相应的投票权重赋予给代理,这样代理才拥有相应的代理投票权。当然喽,你吧投票权给别人了,也就意味着自己没有投票权了。 + +有一点需要注意,如果一个账户已经使用了另外一个账户作为自己的代理,那么这个账户就不能再把自己注册作为代理了。用大白话来说就是,既然你已经让别人来代理你投票了,你就不能代理别人来投票了。。。。。。 + +```c++ +// source file: ./eos/contracts/eosio.system/voting.cpp +// proxy : 想要注册成为代理,或者不想再继续做代理了(太累了是吗?) +// isproxy : true表示想成为代理,false不想做代理了 +void system_contract::regproxy( const account_name proxy, bool isproxy ) { + require_auth( proxy ); + // 在投票队里里面看看,在吗 + auto pitr = _voters.find(proxy); + if ( pitr != _voters.end() ) { + eosio_assert( isproxy != pitr->is_proxy, "action has no effect" ); + eosio_assert( !isproxy || !pitr->proxy, "account that uses a proxy is not allowed to become a proxy" ); + // 赋予或者取消代理资格 + _voters.modify( pitr, 0, [&]( auto& p ) { + p.is_proxy = isproxy; + }); + propagate_weight_change( *pitr ); + } else { + // 把自己加入投票者队列,并且赋予或者取消代理资格 + _voters.emplace( proxy, [&]( auto& p ) { + p.owner = proxy; + p.is_proxy = isproxy; + }); + } +} +``` + + + +### 投票 + +关键点终于来了,这就是投票给候选人。给候选人投票的时候,有一些前提条件。首先,候选者列表是经过了排序的,并且是由低到高的排序;每一个候选者都是处于active状态的。在这个函数的参数里面,proxy和producers只能有一个获得投票,要么是投票者把权重赋予代理这,要么是投票给候选节点。 + +```c++ +// source file: ./eos/contracts/eosio.system/voting.cpp +void system_contract::voteproducer( const account_name voter_name, + const account_name proxy, + const std::vector& producers ) { + require_auth( voter_name ); + update_votes( voter_name, proxy, producers, true ); +} + +// voter_name : 投票者的名字 +// proxy : 代理的名字。如果提供了代理的名字,那么这里就是给把投票者的权重赋予代理 +// producers : 候选者列表 +// voting : 真的要投票吗? +void system_contract::update_votes( const account_name voter_name, + const account_name proxy, + const std::vector& producers, + bool voting ) { + //validate input + if ( proxy ) { + // 如果设置了给代理赋予权重,那么候选列表必须为空,就是一个候选节点都没有 + eosio_assert( producers.size() == 0, "cannot vote for producers and proxy at same time" ); + // 投票者和代理不能是同一个账户 + eosio_assert( voter_name != proxy, "cannot proxy to self" ); + require_recipient( proxy ); + } else { + // 检查:一个投票者,最多只能给30个节点投票 + eosio_assert( producers.size() <= 30, "attempt to vote for too many producers" ); + for( size_t i = 1; i < producers.size(); ++i ) { + // 检查:候选者列表,必须是从低到高排序的 + eosio_assert( producers[i-1] < producers[i], "producer votes must be unique and sorted" ); + } + } + + // 从投票节点列表中,把投票者找出来 + auto voter = _voters.find(voter_name); + // 投票人在投票之前,必须已经做了资产抵押 + eosio_assert( voter != _voters.end(), "user must stake before they can vote" ); /// staking creates voter object + // 如果投票者已经把自己注册为代理了,那么久不能再把自己的投票权委托给其他代理 + eosio_assert( !proxy || !voter->is_proxy, "account registered as a proxy is not allowed to use a proxy" ); + + // 当第一次有人投票的时候,计算并且保存last_vote_weight,然后一直要等到total_activated_stake到达一个阈值的时候,才能取回自己的资产抵押。这里使用last_vote_weight来确定,投票者是不是第一次投票,并且确认他的资产抵押是有效的 + if( voter->last_vote_weight <= 0.0 ) { + _gstate.total_activated_stake += voter->staked; + if( _gstate.total_activated_stake >= min_activated_stake ) { + _gstate.thresh_activated_stake_time = current_time(); + } + } + + // 计算投票者的投票权重 + auto new_vote_weight = stake2vote( voter->staked ); + if( voter->is_proxy ) { + // 如果自己是代理,再把代理权重加上 + new_vote_weight += voter->proxied_vote_weight; + } + + boost::container::flat_map > producer_deltas; + // 必须确认投票者的投票权重大于0 + if ( voter->last_vote_weight > 0 ) { + if( voter->proxy ) { + // 如果自己是代理, + // 则从投票者队列中把自己找出来,并且从代理权重数据中减去本次的投票权重,表示投过票了 + auto old_proxy = _voters.find( voter->proxy ); + eosio_assert( old_proxy != _voters.end(), "old proxy not found" ); //data corruption + _voters.modify( old_proxy, 0, [&]( auto& vp ) { + vp.proxied_vote_weight -= voter->last_vote_weight; + }); + propagate_weight_change( *old_proxy ); + } else { + for( const auto& p : voter->producers ) { + auto& d = producer_deltas[p]; + d.first -= voter->last_vote_weight; + d.second = false; + } + } + } + + if( proxy ) { + // 如果是给代理赋予投票权, + // 则从投票者列表中,找出代理账户,并且给代理账户的代理权重添加相应的投票权重 + auto new_proxy = _voters.find( proxy ); + eosio_assert( new_proxy != _voters.end(), "invalid proxy specified" ); //if ( !voting ) { data corruption } else { wrong vote } + eosio_assert( !voting || new_proxy->is_proxy, "proxy not found" ); + if ( new_vote_weight >= 0 ) { + _voters.modify( new_proxy, 0, [&]( auto& vp ) { + vp.proxied_vote_weight += new_vote_weight; + }); + propagate_weight_change( *new_proxy ); + } + } else { + if( new_vote_weight >= 0 ) { + // 不是赋值给代理的话,就把票投给候选节点 + for( const auto& p : producers ) { + auto& d = producer_deltas[p]; + d.first += new_vote_weight; + d.second = true; + } + } + } + +} +``` + +### Multi_Index + +在投票过程中,有两种非常重要的数据,投票者列表和候选者列表是使用 Multi_Index类来定义的,所以,了解Multi_Index类对于理解上述代码十分重要,在eos的wiki上面有详细介绍。 +https://github.com/eosio/eos/wiki/Persistence-API#the-eosio-multi-index-api + +这里做一个简单的介绍: + +1. Multi-Index API 是EOS提供的访问数据库的C++接口,使智能合约能够读取和修改EOS数据库中的数据; +2. Multi-Index模仿了boost库的boost::multi_index,但又有一些重要不同,eos中使用eosio::multi_index; +3. eosio::multi_index在概念上和传统数据库的“表(table)”类似,数据“行(rows)”是独立的对象,数据“列(columns)”是对象的成员属性; +4. eosio::multi_index提供和传统数据库的“键(key)”类似的成员属性,用于快速查找对象; +5. eosio::multi_index允许使用自定义函数作为索引,但它的返回值是受限制的,只能是支持的键类型; +6. eosio::multi_index支持主键(primary key),但必须是唯一的无符号64位整型(uint64_t); +7. eosio::multi_index按主键排序时,使用升序; +8. Multi-Index表允许多索引排序,最多可以使用16个二级索引; +9. Multi-Index迭代器可以双向迭代,即const_iterator或const_reverse_iterator; +10. 二级索引作为Multi-Index表构造函数的一部分创建,不支持直接构建。 + + +```C++ +// source file: ./eos/contracts/eosiolib/multi_index.hpp +template +class multi_index +{ + //................................... +} + + +// source file: ./eos/contracts/eosio.system/eosio.system.hpp +typedef eosio::multi_index< N(voters), voter_info> voters_table; + +typedef eosio::multi_index< N(producers), producer_info, + indexed_by > + > producers_table; + +class system_contract : public native { + private: + voters_table _voters; + producers_table _producers; +} +``` + +### 区块生产 + +EOS的系统架构,把很多功能都分拆到智能合约和插件中去了,区块的生产模块也被封装到一个plugin之中,名字是producer_plugin。 + +下面这个是区块生产的主流程,从这里可以看出,producer_plugin这个插件在nodeos启动的时候被加载、启动,然后进入了一个生产block的循环。在这里面检查当前链的长度、状态,在收到信的区块的时候检查当前自己是不是生产者,如果是就生产区块。 + +![](./imgs/eos.producer.jpg) + +下面我们来看看源代码: + +```C++ +// source file: ./eos/plugins/producer_plugin/producer_plugin.cpp +void producer_plugin_impl::schedule_production_loop() { + // 获取 chain_plugin + chain::controller& chain = app().get_plugin().chain(); + _timer.cancel(); + std::weak_ptr weak_this = shared_from_this(); + + // 启动区块生产流程 + auto result = start_block(); +} + + +// source file: ./eos/plugins/producer_plugin/producer_plugin.cpp +producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { + // 获取 chain_plugin + chain::controller& chain = app().get_plugin().chain(); + const auto& hbs = chain.head_block_state(); + + // 假设,现在可以出块 + _pending_block_mode = pending_block_mode::producing; + + // 看看情况,现在是不是真的应该我来出块了 + if( !_production_enabled ) { + // 还在同步,现在还不是时候 + _pending_block_mode = pending_block_mode::speculating; + } else if( _producers.find(scheduled_producer.producer_name) == _producers.end()) { + // 看看现在是否轮到自己出块了,对了,现在还没有轮到我 + _pending_block_mode = pending_block_mode::speculating; + } else if (signature_provider_itr == _signature_providers.end()) { + elog("Not producing block because I don't have the private key for ${scheduled_key}", ("scheduled_key", scheduled_producer.block_signing_key)); + // 因为我没有相应的私钥,所以我也没法出块 + _pending_block_mode = pending_block_mode::speculating; + } else if ( _pause_production ) { + elog("Not producing block because production is explicitly paused"); + // 现在处于暂定出块状态 + _pending_block_mode = pending_block_mode::speculating; + } else if ( _max_irreversible_block_age_us.count() >= 0 && irreversible_block_age >= _max_irreversible_block_age_us ) { + elog("Not producing block because the irreversible block is too old [age:${age}s, max:${max}s]", ("age", irreversible_block_age.count() / 1'000'000)( "max", _max_irreversible_block_age_us.count() / 1'000'000 )); + // 还是不能出块,因为最后的一个不可逆块已经是很久以前的事情了,这怎么能出块呢? + _pending_block_mode = pending_block_mode::speculating; + } + + try { + uint16_t blocks_to_confirm = 0; + + if (_pending_block_mode == pending_block_mode::producing) { + if (currrent_watermark_itr != _producer_watermarks.end()) { + auto watermark = currrent_watermark_itr->second; + if (watermark < hbs->block_num) { + blocks_to_confirm = std::min(std::numeric_limits::max(), (uint16_t)(hbs->block_num - watermark)); + } + } + } + + chain.abort_block(); + // 终于到了应该生产区块的时候啦,哈哈哈哈 + chain.start_block(block_time, blocks_to_confirm); + } FC_LOG_AND_DROP(); +} + + +bool producer_plugin_impl::maybe_produce_block() { + try { + // 在这里调用了 produce_block() + produce_block(); + return true; + } FC_LOG_AND_DROP(); +} + + +void producer_plugin_impl::produce_block() { + chain.finalize_block(); + // 对一个块签名 + chain.sign_block( [&]( const digest_type& d ) { + auto debug_logger = maybe_make_debug_time_logger(); + return signature_provider_itr->second(d); + } ); + // 提交一个块 + chain.commit_block(); +} + +``` diff --git a/consensus/ethereum.pow.md b/consensus/ethereum.pow.md new file mode 100644 index 0000000..6b164a9 --- /dev/null +++ b/consensus/ethereum.pow.md @@ -0,0 +1,408 @@ + + +# Ethereum PoW 共识算法概述 + + +## 前言 +Ethereum目前所使用的共识算法还是PoW,未来会切换到PoS的共识算法,本文基于Ethereum 1.8.8 Golang 版本探讨其PoW共识算法。 + +在Ethereum中,PoW共识算法被命名为Ethash,源代码位于 `/go-ethereum/consensus/ethash` 目录下面,与此目录紧密相关的目录有旷工的代码,位于下面这个目录:`/go-ethereum/miner` + +在以太坊的源代码中,共识算法目录里面还包括了下面的目录 `/go-ethereum/consensus/clique` 这个其实现的PoA共识算法,但是并没有适用于以太坊的主网里面,对这个共识算法,本文不做讨论。 + + + + +## 主流程 +![](./imgs/eth.mining.diagram.jpg) + +上面这幅图就是以太坊里面挖矿的主流程。从这个流程图上我们可以看出来,首先会创建一个Miner对象,在Miner对象创建过程中,会创建worker对象以及创建CpuAgent对象并且注册到worker对象。然后使用goroutinue来启动miner.Update()和miner.start()方法。 + +在创建worker对象的过程中,会创建三个channel并监听相应的event,这三个channel都是block相关的,然后使用goroutinue来启动worker.Update()和worker.Wait()方法。 + +在worker.Start()方法中,会遍历其所持有的Ageng对象并依次调用agent.Start()方法。在agent.Start()中会调用ageng.Update()和agent.mine()方法, 在agent.mine()中会去调用Agent中持有的共识算法引擎ethash的Seal()方法执行具体的寻找nonce去符合挖矿难度目标的要求。 + +下面我们沿着这个主流程一步一步的对这一部分源码进行“挖矿” + +## Miner + +miner package负责向外提供一个“挖矿”得到的新区块,这个package中有一个Miner对象,它作为公共类型,向外暴露mine功能。Miner对象用来对worker进行管理, 订阅外部事件,控制worker的启动和停止。 + + +数据结构 + +```go +type Miner struct { + mux *event.TypeMux // 事件锁,事件发生时,会有一个TypeMux将时间分派给注册的接收者 + + worker *worker // worker, 顾名思义,干活的人 + + coinbase common.Address // 节点地址 + mining int32 // 挖矿进行中的状态 + eth Backend // Backend对象是一个自定义接口封装了所有挖矿所需方法。 + engine consensus.Engine // 获得共识算法的工具对象,以提供后续共识相关操作使用。 + + canStart int32 // 是否能够开始挖矿操作 + shouldStart int32 // 同步以后是否应该开始挖矿 +} +``` + +Miner对象在构造的过程中, 针对新对象miner的各个成员变量初始化完成后,会紧跟着创建worker对象,然后创建了一个CpuAgent并且把这个CpuAgent注册给了worker对象。然后用一个goroutine启动了Miner对象自身的miner.Update()函数 + + +```go +func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *Miner { + miner := &Miner{ + eth: eth, + mux: mux, + engine: engine, + // 创建worker对象 + worker: newWorker(config, engine, common.Address{}, eth, mux), + canStart: 1, + } + // 创建CpuAgent并且注册给worker对象 + miner.Register(NewCpuAgent(eth.BlockChain(), engine)) + // 用goroutine启动miner.Update()函数 + go miner.update() + + return miner +} +``` + +Miner对象的Update()方法订阅(监听)了downloader的事件。当收到downloader.StartEvent时,意味者此时本节点正在从其他节点下载新区块,这时miner会立即停止进行中的挖掘工作,并继续监听; + +如果收到downloader.DoneEvent或downloader.FailEvent时,意味本节点的下载任务已结束-无论下载成功或失败-此时都可以开始挖掘新区块,并且此时会退出Downloader事件的监听。注意这个goroutine是一个一次性的循环, 只要接收到一次downloader.DoneEvent或者 downloader.FailedEvent事件, 就会设置canStart为1. 并退出循环, 这是为了避免黑客恶意的 DOS攻击,让你不断的处于异常状态 + +从这里的逻辑可以看出,对于任何一个Ethereum网络中的节点来说,挖掘一个新区块和从其他节点下载、同步一个新区块,根本是相互冲突的。这样的规定,保证了在某个节点上,一个新区块只可能有一种来源,这可以大大降低可能出现的区块冲突,并避免全网中计算资源的浪费。 + +```go +func (self *Miner) update() { + events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{}) +out: + for ev := range events.Chan() { + switch ev.Data.(type) { + case downloader.StartEvent: + atomic.StoreInt32(&self.canStart, 0) + if self.Mining() { + self.Stop() + atomic.StoreInt32(&self.shouldStart, 1) + log.Info("Mining aborted due to sync") + } + case downloader.DoneEvent, downloader.FailedEvent: + shouldStart := atomic.LoadInt32(&self.shouldStart) == 1 + + atomic.StoreInt32(&self.canStart, 1) + atomic.StoreInt32(&self.shouldStart, 0) + if shouldStart { + // 启动挖矿 + self.Start(self.coinbase) + } + // 取消订阅,只对这些消息监听一次 + events.Unsubscribe() + // 立即停止并且忽略其余的事件 + break out + } + } +} +``` + +在miner.Start()方法中,主要的工作是启动了worker.start()来开始挖矿,以及启动worker.commitNewWork()来提交新的挖矿任务。 + +```go +func (self *Miner) Start(coinbase common.Address) { + atomic.StoreInt32(&self.shouldStart, 1) // 是是否应该启动 + self.worker.setEtherbase(coinbase) + self.coinbase = coinbase + + if atomic.LoadInt32(&self.canStart) == 0 { // 是否能够启动, + log.Info("Network syncing, will start miner afterwards") + return + } + atomic.StoreInt32(&self.mining, 1) + + log.Info("Starting mining operation") + self.worker.start() // 启动worker 开始挖矿 + self.worker.commitNewWork() //提交新的挖矿任务。 +} +``` + + + + +## worker + +worker 哈哈,顾名思义,这是具体干活的对象,它负责全部具体的挖矿工作流程。其包含了多个属性,都是与具体的挖矿操作相关的,其中就包括了链本身的属性以及区块数据结构的属性。内部包含多个agent,可以包含之前提到的agent和remote_agent。 worker同时负责构建区块和对象。同时把任务提供给agent。 + +worker对象负责管理mine过程;worker内部有一组Agent接口类型对象,每个Agent都可以完成单个区块的mine;Work结构体主要用以携带数据,被视为挖掘一个区块时所需的数据环境。 + +主要的数据传输发生在worker和它的Agent(们)之间:在合适的时候,worker把一个Work对象发送给每个Agent,然后任何一个Agent完成mine时,将一个经过授权确认的Block加上那个更新过的Work,组成一个Result对象发送回worker。 + +有意思的是<>接口,尽管调用方worker内部声明了一个Agent数组,但目前只有一个实现类CpuAgent的对象会被加到该数组,可能这个Agent数组是为将来的扩展作的预留吧。CpuAgent通过全局的<>对象,借助共识算法完成最终的区块授权。 + + +Agent接口,可以把自身注册到worker。在PoW算法中,创建了一个CpuAgent来完成具体的工作。 + +```go +type Agent interface { + Work() chan<- *Work + SetReturnCh(chan<- *Result) + Stop() + Start() + GetHashRate() int64 +} + +// worker是负责将消息应用到新状态的主要对象 +type worker struct { + config *params.ChainConfig + engine consensus.Engine + mu sync.Mutex + // update loop + mux *event.TypeMux + txCh chan core.TxPreEvent // 用来接受txPool里面的交易的通道 + txSub event.Subscription // 用来接受txPool里面的交易的订阅器 + chainHeadCh chan core.ChainHeadEvent // 用来接受区块头的通道 + chainHeadSub event.Subscription + chainSideCh chan core.ChainSideEvent // 用来接受一个区块链从规范区块链移出的通道 + chainSideSub event.Subscription + wg sync.WaitGroup + + agents map[Agent]struct{} // worker所拥有的agent的map集合 + recv chan *Result // agent会把结果发送到这个通道 + + eth Backend // eth的协议 + chain *core.BlockChain // 区块链 + proc core.Validator // 区块链验证器 + chainDb ethdb.Database // 区块链数据库 + + coinbase common.Address // 挖矿者的地址 + extra []byte // + + currentMu sync.Mutex + current *Work + + uncleMu sync.Mutex + possibleUncles map[common.Hash]*types.Block //可能的叔父节点 + + unconfirmed *unconfirmedBlocks // 本地挖出的待确认块 + + // atomic status counters + mining int32 + atWork int32 +} +``` + + +#### newWorker + +worker对象是在创建Miner对象的时候创建的,它会根据Miner结构体成员属性依次给worker对象属性赋值,然后创建了三个监听事件的channel并且监听相应的事件。其部分代码如下(完整代码请参考go-ethereum源代码): + +```go +func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker { + worker := &worker{ + // TxPreEvent事件是TxPool发出的事件,代表一个新交易tx加入到了交易池中,这时候如果work空闲会将该笔交易收进work.txs,准备下一次打包进块 + txCh: make(chan core.TxPreEvent, txChanSize), + // ChainHeadEvent事件,代表已经有一个块作为链头,此时work.update函数会监听到这个事件,则会继续挖新的区块 + chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize), + // ChainSideEvent事件,代表有一个新块作为链的旁支,会被放到possibleUncles数组中,可能称为叔块 + chainSideCh: make(chan core.ChainSideEvent, chainSideChanSize), + // 区块链数据库 + chainDb: eth.ChainDb(), + // 存放可能称为下一个块的叔块数组 + possibleUncles: make(map[common.Hash]*types.Block), + // 返回一个数据结构,包括追踪当前未被确认的区块 + unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth), + } + // 注册TxPreEvent事件到tx pool交易池 + worker.txSub = eth.TxPool().SubscribeTxPreEvent(worker.txCh) + // 注册worker.chainHeadCh事件到blockchain + worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh) + // 注册worker.chainSideCh事件到 + worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh) + go worker.update() + + go worker.wait() + worker.commitNewWork() + + return worker +} +``` + +我们看到在newWorker的最后,通过goroutinue启动了worker.update()个worker.wait()两个方法,最后还调用了worker.commitNewWork()。 + + + +worker.update()分别监听ChainHeadEvent,ChainSideEvent,TxPreEvent几个事件,每个事件会触发worker不同的操作。 + +ChainHeadEvent是指区块链中已经加入了一个新的区块作为整个链的链头,这时worker的回应是立即开始准备挖掘下一个新区块;ChainHeadEvent并不一定是外部源发出。由于worker对象有个成员变量chain(eth.BlockChain),所以当worker自己完成挖掘一个新区块,并把它写入数据库,加进区块链里成为新的链头时,worker自己也可以调用chain发出一个ChainHeadEvent,从而被worker.update()函数监听到,进入下一次区块挖掘。 + +ChainSideEvent指区块链中加入了一个新区块作为当前链头的旁支,worker会把这个区块收纳进possibleUncles[]数组,作为下一个挖掘新区块可能的Uncle之一; + +TxPreEvent是TxPool对象发出的,指的是一个新的交易tx被加入了TxPool,这时如果worker没有处于挖掘中,那么就去执行这个tx,并把它收纳进Work.txs数组,为下次挖掘新区块备用。 + +下面是worker.update()的部分代码(完整代码请参考go-ethereum源代码)。 + +```go +func (self *worker) update() { + for { + select { + // Handle ChainHeadEvent 当接收到一个区块头的信息的时候,马上开启挖矿服务。 + case <-self.chainHeadCh: + + // Handle ChainSideEvent 接收不在规范的区块链的区块,加入到潜在的叔父集合 + case ev := <-self.chainSideCh: + + // Handle TxPreEvent 接收到txPool里面的交易信息的时候。 + case ev := <-self.txCh: + // 如果当前没有挖矿, 那么把交易应用到当前的状态上,以便马上开启挖矿任务。 + } + } +} +``` + + +worker.wait()会在一个channel处一直等待Agent完成挖掘发送回来的新Block和Work对象。这个Block会被写入数据库,加入本地的区块链试图成为最新的链头。注意,此时区块中的所有交易,假设都已经被执行过了,所以这里的操作,不会再去执行这些交易对象。 +当这一切都完成,worker就会发送一条事件(NewMinedBlockEvent{}),等于通告天下:我挖出了一个新区块!这样监听到该事件的其他节点,就会根据自身的状况,来决定是否接受这个新区块成为全网中公认的区块链新的链头。至于这个公认过程如何实现,就属于共识算法的范畴了。 + +commitNewWork()会在worker内部多处被调用,注意它每次都是被直接调用,并没有以goroutine的方式启动。commitNewWork()内部使用sync.Mutex对全部操作做了隔离。这个函数的基本逻辑如下: +- 准备新区块的时间属性Header.Time,一般均等于系统当前时间,不过要确保父区块的时间(parentBlock.Time())要早于新区块的时间,父区块当然来自当前区块链的链头了。 +- 创建新区块的Header对象,其各属性中:Num可确定(父区块Num + 1);Time可确定;ParentHash可确定;其余诸如Difficulty,GasLimit等,均留待之后共识算法中确定。 +- 调用Engine.Prepare()函数,完成Header对象的准备。 +- 根据新区块的位置(Number),查看它是否处于DAO硬分叉的影响范围内,如果是,则赋值予header.Extra。 +- 根据已有的Header对象,创建一个新的Work对象,并用其更新worker.current成员变量。 +- 如果配置信息中支持硬分叉,在Work对象的StateDB里应用硬分叉。 +- 准备新区块的交易列表,来源是TxPool中那些最近加入的tx,并执行这些交易。 +- 准备新区块的叔区块uncles[],来源是worker.possibleUncles[],而possibleUncles[]中的每个区块都从事件ChainSideEvent中搜集得到。注意叔区块最多有两个。 +- 调用Engine.Finalize()函数,对新区块“定型”,填充上Header.Root, TxHash, ReceiptHash, UncleHash等几个属性。 +- 如果上一个区块(即旧的链头区块)处于unconfirmedBlocks中,意味着它也是由本节点挖掘出来的,尝试去验证它已经被吸纳进主干链中。 +- 把创建的Work对象,通过channel发送给每一个登记过的Agent,进行后续的挖掘。 + +以上步骤中,4和6都是仅仅在该区块配置中支持DAO硬分叉,并且该区块的位置正好处于DAO硬分叉影响范围内时才会发生;其他步骤是普遍性的。commitNewWork()完成了待挖掘区块的组装,block.Header创建完毕,交易数组txs,叔区块Uncles[]都已取得,并且由于所有交易被执行完毕,相应的Receipt[]也已获得。万事俱备,可以交给Agent进行‘挖掘’了。 + +下面是worker.commitNewWork()的部分代码(完整代码请参考go-ethereum源代码)。 + +```go +func (self *worker) commitNewWork() { + // 不能出现比parent的时间还早的情况 + if parent.Time().Cmp(new(big.Int).SetInt64(tstamp)) >= 0 { + tstamp = parent.Time().Int64() + 1 + } + + // 我们的时间不要超过现在的时间太远, 那么等待一段时间, + // 感觉这个功能完全是为了测试实现的, 如果是真实的挖矿程序,应该不会等待。 + if now := time.Now().Unix(); tstamp > now+1 { + } + + // 只有当我们挖矿的时候才设置coinbase + if atomic.LoadInt32(&self.mining) == 1 { + } + + // 根据我们是否关心DAO硬分叉来决定是否覆盖额外的数据。 + if daoBlock := self.config.DAOForkBlock; daoBlock != nil { + // 检查区块是否在 DAO硬分叉的范围内 [daoblock,daoblock+limit] + limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange) + if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 { + // 如果我们支持DAO 那么设置保留的额外的数据 + if self.config.DAOForkSupport { + header.Extra = common.CopyBytes(params.DAOForkBlockExtra) + } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) { + header.Extra = []byte{} + } + } + } + + // 用新的区块头来设置当前的状态 + err := self.makeCurrent(parent, header) + //得到阻塞的资金 + pending, err := self.eth.TxPool().Pending() + if err != nil { + log.Error("Failed to fetch pending transactions", "err", err) + return + } + // 创建交易。 这个方法后续介绍 + txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending) + // 提交交易 这个方法后续介绍 + work.commitTransactions(self.mux, txs, self.chain, self.coinbase) + + // 使用给定的状态来创建新的区块,Finalize会进行区块奖励等操作 + if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts); err != nil { + } +} +``` + +## CpuAgent + +agent 是具体执行挖矿的对象。它执行的流程就是,接受计算好了的区块头, 计算mixhash和nonce, 把挖矿好的区块头返回。与mine过程相关的函数,主要是update()和mine(): + +CpuAgent.update()就是worker.commitNewWork()结束后发出Work对象的会一直监听相关channel,如果收到Work对象(显然由worker.commitNewWork()结束后发出),就启动mine()函数;如果收到停止(mine)的消息,就退出一切相关操作。 + +CpuAgent.mine()会直接调用Engine.Seal()函数,利用Engine实现体的共识算法对传入的Block进行最终的授权,如果成功,就将Block同Work一起通过channel发还给worker,那边worker.wait()会接收并处理。 +显然,这两个函数都没做什么实质性工作,它们只是负责调用接口实现体,待授权完成后将区块数据发送回worker。挖掘出一个区块的真正奥妙全在Engine实现体所代表的共识算法里。 + +在PoW算法中,CpuAgent对象中的engine就是Ethash的对象,下面我们来看Ethash对象。 + + + +## Ethash + +Ethash是PoW的共识引擎,它实现了Engine接口,在Engine接口声明的函数中, +- VerifyHeader(),VerifyHeaders(),VerifyUncles()用来验证区块相应数据成员是否合理合规,可否放入区块; +- Prepare()函数往往在Header创建时调用,用来对Header.Difficulty等属性赋值; +- Finalize()函数在区块区块的数据成员都已具备时被调用,比如叔区块(uncles)已经具备,全部交易Transactions已经执行完毕,全部收据(Receipt[])也已收集完毕,此时Finalize()会最终生成Root,TxHash,UncleHash,ReceiptHash等成员。 +- Seal()函数可对一个调用过Finalize()的区块进行授权或封印,并将封印过程产生的一些值赋予区块中剩余尚未赋值的成员(Header.Nonce, Header.MixDigest)。Seal()成功时返回的区块全部成员齐整,可视为一个正常区块,可被广播到整个网络中,也可以被插入区块链等。所以,对于挖掘一个新区块来说,所有相关代码里Engine.Seal()是其中最重要,也是最复杂的一步。 +- VerifySeal()函数基于跟Seal()完全一样的算法原理,通过验证区块的某些属性(Header.Nonce,Header.MixDigest等)是否正确,来确定该区块是否已经经过Seal操作。 +在两种共识算法的实现中,Ethash是产品环境下以太坊真正使用的共识算法,Clique主要针对以太坊的测试网络运作,两种共识算法的差异,主要体现在Seal()的实现上。 + + +Ethash.Seal()函数实现中,会以多线程(goroutine)的方式并行调用mine()函数,线程个数等于Ethash.threads;如果Ethash.threads被设为0,则Ethash选择以本地CPU中的总核数作为开启线程的个数。 + +下面是ethash.Seal()的部分代码(完整代码请参考go-ethereum源代码)。 + +```go +// Seal方法通过调用mine()尝试去寻找一个nonce值,去符合产生一个新区块的难度目标 +func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) { + // 如果threads数等于0,就获取当前CPU的核心数 + if threads == 0 { + threads = runtime.NumCPU() + } + + var pend sync.WaitGroup + for i := 0; i < threads; i++ { + pend.Add(1) + go func(id int, nonce uint64) { + defer pend.Done() + // 调用自身的 mine 函数执行挖矿动作 + ethash.mine(block, id, nonce, abort, found) + }(i, uint64(ethash.rand.Int63())) + } +} +``` + + +下面是mine()函数的主要业务逻辑。入参id是线程编号,用来发送log告知上层;函数内部首先定义一组局部变量,包括之后调用hashimotoFull()时传入的hash、nonce、巨大的辅助数组dataset,以及结果比较的target;然后是一个无限循环,每次调用hashimotoFull()进行一系列复杂运算,一旦它的返回值符合条件,就复制Header对象(深度拷贝),并赋值Nonce、MixDigest属性,返回经过授权的区块。注意到在每次循环运算时,nonce还会自增+1,使得每次循环中的计算都各不相同。 +这里hashimotoFull()函数通过调用hashimoto()函数完成运算,而同时还有另外一个类似的函数hashimoLight()函数。 + +下面是ethash.mine()的部分代码(完整代码请参考go-ethereum源代码)。 + +```go +// mine()会具体去尝试去寻找一个nonce值,去符合产生一个新区块的难度目标 +func (ethash *Ethash) mine(block *Block, id int, seed uint64, abort chan struct{}, found chan *Block) { + for { + select { + case <-abort: + ...; return + default: + // 计算这个nonce的hash值 + digest, result := hashimotoFull(dataset, hash, nonce) + if new(big.Int).SetBytes(result).Cmp(target) <= 0 { + // 找到了合适的nonce值 + header = types.CopyHeader(header) + header.Nonce = types.EncodeNonce(nonce) + header.MixDigest = common.BytesToHash(digest) + found<- block.WithSeal(header) + return + } + } + nonce++ + } +} +``` diff --git a/consensus/fabric.pbft.md b/consensus/fabric.pbft.md new file mode 100644 index 0000000..dab7454 --- /dev/null +++ b/consensus/fabric.pbft.md @@ -0,0 +1,584 @@ + + +# Hyperledger Fabric PBFT 共识算法解析 + + +从Fabric 1.0开始,把PBFT共识算法的代码给去掉了,最新的代码里面并没有PBFT,所以本文是从Hyperledger Fabric v0.6.1版本源代码的基础上来解析的。 + + +## PBFT 暴露的接口 +PBFT 算法的核心代码在这个目录下面:`/fabric/consensus/pbft`,但是PBFT算法模块对外的接口位于这个文件之中:`/fabric/consensus/consensus.go` + +```go +// /fabric/consensus/consensus.go +// 定义了在异步执行和状态转换时候的回调方法 +type ExecutionConsumer interface { + // 执行完成的时候调用 + Executed(tag interface{}) + // commit 完成的时候调用 + Committed(tag interface{}, target *pb.BlockchainInfo) + // 回滚操作完成的时候调用 + RolledBack(tag interface{}) + // 状态转换完成的时候调用 + StateUpdated(tag interface{}, target *pb.BlockchainInfo) +} + +// 下面这个接口用于从网络接口消息,任何共识算法插件都需要实现这个接口 +type Consenter interface { + // 对所有接收到的消息连续的调用这个方法 + RecvMsg(msg *pb.Message, senderHandle *pb.PeerID) error + ExecutionConsumer +} + +type Stack interface { + // 网络消息发送和接收接口 + NetworkStack + // Sign和Verify接口 + SecurityUtils + // 事件消息处理接口 + Executor + // 交易处理接口 + LegacyExecutor + // 控制ledger的状态 + LedgerManager + // 操作blockchain + ReadOnlyLedger + // 操作共识状态 + StatePersistor +} +``` + +每个共识插件都需要实现Consenter接口,包括RecvMsg函数和ExecutionConsumer接口里的函数。 + +Consenter是EngineImpl的一个成员,EngineImpl是接口Engine的一个实例,是在peer启动的时候创建的,连同Impl的其他成员一起注册到gRPC服务中。当通过gRPC收到ProcessTransaction消息时,最终会调用Consenter的RecvMsg处理交易信息。 + +ExecutionConsumer接口是专门处理事件消息的,它是Stack的成员Executor的一个接口。coordinatorImpl是Executor的一个实例,在实例化coordinatorImpl的时候同时设置自身为成员变量Manager的事件消息接收者,然后启动一个协程循环处理接收到的事件,根据不同的事件类型,调用ExecutionConsumer的不同函数。特别说明一下,事件在内部是channel实现的生产者/消费者模型,只有一个缓冲区,如果处理不及时会出现消息等待的情况,在实际产品化过程中需要进行优化。 + +在 EngineImpl 中包含了实现上述接口对象的reference。 +```go +// /fabric/helper/engine.go +// EngineImpl 定义了具体的共识算法引擎,包含consensus.Consenter, PeerEndpoint 和 MessageFan 接口的实现对象 +type EngineImpl struct { + consenter consensus.Consenter + helper *Helper + peerEndpoint *pb.PeerEndpoint + consensusFan *util.MessageFan +} +``` + + +## 共识引擎创建流程 +下面这是共识引擎创建的主流程 +![](./imgs/fabric.pbft.engine.creation.flow.jpg) + +从这张图中,我们可以看到,在 peer node 节点启动的过程中,会调用 consensus.engine.helper.GetEngine(...), + +```go +// /fabric/peer/node/start.go +func serve(args []string) error { + // ................ 省略无关代码 若干 行 + peerServer, err = peer.NewPeerWithEngine(secHelperFunc, helper.GetEngine) +} + +// /fabric/core/peer/peer.go +func NewPeerWithEngine(secHelperFunc func() crypto.Peer, engFactory EngineFactory) (peer *Impl, err error) { + // ................ 省略无关代码 若干 行 + peer.engine, err = engFactory(peer) +} +``` + + +GetEngine(...) 的作用是进行共识模块的初始化,首先会创建 EngineImpl 对象,然后会调用 consensus.controller.NewConsenter(...) 方法创建具体的Consenter。最后会启动一个goroutine等待消息进入。 + +在NewConsenter(...)方法中,会去调用consensus.pbft.GetPlugin(...)方法去创建共识引擎插件 + +```go +// /fabric/consensus/helper/engine.go +func GetEngine(coord peer.MessageHandlerCoordinator) (peer.Engine, error) { + // ................ 省略无关代码 若干 行 + engine = new(EngineImpl) + engine.consenter = controller.NewConsenter(engine.helper) + + go func() { + // 启动goroutine等待消息进入 + for msg := range engine.consensusFan.GetOutChannel() { + engine.consenter.RecvMsg(msg.Msg, msg.Sender) + } + }() +}) + +// /fabric/consensus/controller/controller.go +func NewConsenter(stack consensus.Stack) consensus.Consenter { + // ................ 省略无关代码 若干 行 + return pbft.GetPlugin(stack) +} +``` + + +下一步,会根据在配置文件中的"general.mode"配置项来创建匹配的对象,在这个版本中仅支持batch方式。 + +在newObcBatch()方法中,首先会初始化一个obcbatch对象。这个batch对象的作用就是用来做request缓存,提高transaction的执行效率,缓存存储在batchStore里。 + +调用 consensus.util.event.NewManagerImpl() 去创建一个向PBFT共识引擎发送消息的ManagerImpl对象,并且把前一步创建的pbftCore对象设置给它作为Receiver对象,即接收message的对象。 + +最后,会创建pbftCore对象,到一个pbftcore的一个实例,这个是算法的核心模块。并此时会启动一个batchTimer(这个batchTimer是一个计时器,当batchTimer timeout后会触发一个sendbatch操作,这个只有primary节点才会去做)。当然此时会创建一个事件处理机制,这个事件处理机制是各个模块沟通的桥梁。 + + +```go +// /fabric/consensus/pbft/pbft.go +func GetPlugin(c consensus.Stack) consensus.Consenter { + // ................ 省略无关代码 若干 行 + pluginInstance = New(c) +} + +func New(stack consensus.Stack) consensus.Consenter { + // ................ 省略无关代码 若干 行 + switch strings.ToLower(config.GetString("general.mode")) { + case "batch": + return newObcBatch(id, config, stack) + default: + } +} + +func newObcBatch(id uint64, config *viper.Viper, stack consensus.Stack) *obcBatch { + // ................ 省略无关代码 若干 行 + op := &obcBatch{ + obcGeneric: obcGeneric{stack: stack}, + } + op.manager = events.NewManagerImpl() + op.manager.SetReceiver(op) + op.pbft = newPbftCore(id, config, op, etf) +} +``` + +在初始化pbftcore时,在把所用配置读进的同时,创建了三个timer: + +```go +func newPbftCore(id uint64, config *viper.Viper, consumer innerStack, etf events.TimerFactory) *pbftCore { + // ................ 省略细节代码N行 + + // newViewTimer对应于viewChangeTimerEvent{},当这个timer在一定时间没有close时,就会触发一个viewchange事件 + instance.newViewTimer = etf.CreateTimer() + // vcResendTimer对应viewChangeResendTimerEvent,发出viewchange过时时会触发一个将viewchange从新发送 + instance.vcResendTimer = etf.CreateTimer() + // nullRequestTimer对应nullRequestEvent,如果主节点长期没有发送preprepare消息,也就是分配了seq的reqBatch。它timeout就认为主节点挂掉了然后发送viewchange消息 + instance.nullRequestTimer = etf.CreateTimer() + + //网络中验证器的最大数量赋值 + //N是所有replicas的集合,每一个replica用一个整数来表示,依次为 + // { 0, …, |N - 1 } + // 简单起见,我们定义 + // |N = 3f + 1 + // f 是最大可容忍的faulty节点 + // 另外我们将一个view中的primary节点定义为replica p, + // p = v mod |N + // v 是view的编号,从0开始一直连续下去,这样可以理解为从replica 0 到 replica |N-1 依次当primary节点,当每一次view change发生时 + instance.N = config.GetInt("general.N") + + //默认的最大容错数量赋值 + instance.f = config.GetInt("general.f") + if instance.f*3+1 > instance.N { + //默认的最大容错数量大于网络中验证器的最大数量 + panic(............) + } + + //检查点时间段赋值 + instance.K = uint64(config.GetInt("general.K")) + + //自动视图改变的时间段 + instance.viewChangePeriod = uint64(config.GetInt("general.viewchangeperiod")) + //这个节点是否故意充当拜占庭;testnet用于调试 + instance.byzantine = config.GetBool("general.byzantine") + //请求过程超时 + instance.requestTimeout, err = time.ParseDuration(config.GetString("general.timeout.request")) + + //重发视图改变之前超时 + instance.vcResendTimeout, err = time.ParseDuration(config.GetString("general.timeout.resendviewchange")) + + //新的视图超时 + instance.newViewTimeout, err = time.ParseDuration(config.GetString("general.timeout.viewchange")) + + //超时持续 + instance.nullRequestTimeout, err = time.ParseDuration(config.GetString("general.timeout.nullrequest")) + + //广播过程超时 + instance.broadcastTimeout, err = time.ParseDuration(config.GetString("general.timeout.broadcast")) + + //查看view发生 + instance.activeView = true + //replicas的数量; PBFT `|R|` + instance.replicaCount = instance.N + + //跟踪法定证书请求 + instance.certStore = make(map[msgID]*msgCert) + //跟踪请求批次 + instance.reqBatchStore = make(map[string]*RequestBatch) + //跟踪检查点设置 + instance.checkpointStore = make(map[Checkpoint]bool) + //检查点状态; 映射lastExec到全局hash + instance.chkpts = make(map[uint64]string) + //跟踪视view change消息 + instance.viewChangeStore = make(map[vcidx]*ViewChange) + + //跟踪我们接收后者发送的最后一个新视图 + instance.newViewStore = make(map[uint64]*NewView) + // initialize state transfer + //观察每一个replica最高薄弱点序列数 + instance.hChkpts = make(map[uint64]uint64) + //检查点状态; 映射lastExec到全局hash + instance.chkpts[0] = "XXX GENESIS" + // 在我们使用视图改变期间最后超时 + instance.lastNewViewTimeout = instance.newViewTimeout + //跟踪我们是否正在等待请求批处理执行 + instance.outstandingReqBatches = make(map[string]*RequestBatch) + //对于所有已经分配我们可能错过的在视图改变期间的非检查点的请求批次 + instance.missingReqBatches = make(map[string]bool) + //将变量的值恢复到初始状态 + instance.restoreState() + // 执行视图改变的下一个序号 + instance.viewChangeSeqNo = ^uint64(0) + //更新视图改变序列号 + instance.updateViewChangeSeqNo() + +} +``` + +## externalEventReceiver +在 `fabric/consensus/pbft/external.go` 文件中,externalEventReceiver struct 实现了上述PBFT共识引擎的接口,Fabric通过这里的调用来向PBFT引擎传递消息。 + +由于 externalEventReceiver 设置的 Receiver 是 obcbatch, 所以外面所有事件交由 obcbatch.ProcessEvent 处理,obcbatch.ProcessEvent 转发给 obcbatch.processMessage,然后调用 op.submitToLeader(req) + +submitToLeader 存下该请求(storeOutstanding(req)),并广播(防止自己处于错误的 view),然后如果自己是主节点的话,执行 leaderProcReq,它的策略主要是在请求数达到 op.batchSize 的时候才返回 events.Event,否则返回空。当返回不为空时,processMessage 会返回一个非空事件,然后传递到 ProcessEvent进入 default 分支,交给 pbft-core 的 ProcessEvent 模块处理 (RequestBatch 事件) + +```go +// fabric/consensus/pbft/external.go +// 当收到消息时(和 noops 消息类型一样),调用 externalEventReceiver.RecvMsg +func (eer *externalEventReceiver) RecvMsg(ocMsg *pb.Message, senderHandle *pb.PeerID) error { + eer.manager.Queue() <- batchMessageEvent{ + msg: ocMsg, + sender: senderHandle, + } + return nil +} + +// fabric/consensus/pbft/batch.go +func (op *obcBatch) submitToLeader(req *Request) events.Event { + // 把这个request消息广播到网络中去 + op.broadcastMsg(&BatchMessage{Payload: &BatchMessage_Request{Request: req}}) + // 保存这个请求 + op.reqStore.storeOutstanding(req) + // 判断自己是不是主节点 + if op.pbft.primary(op.pbft.view) == op.pbft.id && op.pbft.activeView { + return op.leaderProcReq(req) + } + return nil +} +``` + + + +## 事件处理 + +在consensus.pbft.pbftCore.ProcessEvent(....) 方法中处理所有接收到的消息。 + +```go +// /fabric/consensus/util/events/events.go +// eventLoop() 运行在一个goroutinye中,一直监听这消息, +// 不断的从em.events里取出事件,通过Inject注射给对应的接收者 +// 直到pbftCore.ProcessEvent(...) +func (em *managerImpl) eventLoop() { + // ................ 省略细节代码N行 + select { + case next := <-em.events: + em.Inject(next) + } +} + +// /fabric/consensus/util/events/events.go +func (em *managerImpl) Inject(event Event) { + SendEvent(em.receiver, event) +} + +// /fabric/consensus/util/events/events.go +func SendEvent(receiver Receiver, event Event) { + next := event + for { + // 这个receiver就是下一步的pbftCore对象 + next = receiver.ProcessEvent(next) + if next == nil { + break + } + } +} + +// /fabric/consensus/pbft/pbft-core.go +func (instance *pbftCore) ProcessEvent(e events.Event) events.Event { + // ................ 省略细节代码N行 + switch et := e.(type) { + case viewChangeTimerEvent: + case *pbftMessage: + case pbftMessageEvent: + case *RequestBatch: + err = instance.recvRequestBatch(et) + case *PrePrepare: + err = instance.recvPrePrepare(et) + case *Prepare: + err = instance.recvPrepare(et) + case *Commit: + err = instance.recvCommit(et) + default: + } +} +``` + +## PBFT协议过程 + +#### 主节点收到 Client Request + +- 存入到 reqBatchStore,outstandingReqBatches, 并持久化 (instance.consumer.StoreState(k,v) ) +- 打开一个与 hash(req) 相关的计时器 +- 判断当前节点是不是主节点,如果是 sendPrePrepare + +```go +// /fabric/consensus/pbft/pbft-core.go +func (instance *pbftCore) recvRequestBatch(reqBatch *RequestBatch) error { + // ................ 省略细节代码N行 + + // 首先计算出这个requestBatch的hash,并且保存下来 + digest := hash(reqBatch) + instance.reqBatchStore[digest] = reqBatch + instance.outstandingReqBatches[digest] = reqBatch + instance.persistRequestBatch(digest) + + if instance.activeView { + instance.softStartTimer(instance.requestTimeout, fmt.Sprintf("new request batch %s", digest)) + } + + if instance.primary(instance.view) == instance.id && instance.activeView { + instance.nullRequestTimer.Stop() + // 判断出当前自己是主节点,发送prePrepare消息 + instance.sendPrePrepare(reqBatch, digest) + } else { + logger.Debugf("Replica %d is backup, not sending pre-prepare for request batch %s", instance.id, digest) + } + return nil +} +``` + +#### 主节点发送 PrePrepare 过程 + +主节点在收到request消息之后,首先会进行有效性判断,通过之后就会构造prePrepare消息,本地保存之后,就广播出去啦。 + +```go +// /fabric/consensus/pbft/pbft-core.go +func (instance *pbftCore) sendPrePrepare(reqBatch *RequestBatch, digest string) { + // ................ 省略细节代码N行 + + // 先得到序列号 n + n := instance.seqNo + 1 + for _, cert := range instance.certStore { + if p := cert.prePrepare; p != nil { + if p.View == instance.view && p.SequenceNumber != n && p.BatchDigest == digest && digest != "" { + // 如果发现已经收到其他摘要相同 view 相同而序列号 n 不同的,则返回 + return + } + } + } + + if !instance.inWV(instance.view, n) || n > instance.h+instance.L/2 { + // 当 n 不在 watermarks 间,或 n > h + L/2 返回 + return + } + + if n > instance.viewChangeSeqNo { + // 当 n 大于 viewChangeSeqNo, 因为将要 view-change 主节点,返回 + return + } + + // 构造 pre-prepare 消息 + instance.seqNo = n + preprep := &PrePrepare{ + View: instance.view, + SequenceNumber: n, + BatchDigest: digest, + RequestBatch: reqBatch, + ReplicaId: instance.id, + } + + // 将 prePrepare, digest 存入 cert 里,并持久化 + cert := instance.getCert(instance.view, n) + cert.prePrepare = preprep + cert.digest = digest + instance.persistQSet() + + // 广播 pre-Prepare 消息 + instance.innerBroadcast(&Message{Payload: &Message_PrePrepare{PrePrepare: preprep}}) + +} + +``` + + + +#### 收到PrePrepare消息的处理流程 + + +```go +// /fabric/consensus/pbft/pbft-core.go +func (instance *pbftCore) recvPrePrepare(preprep *PrePrepare) error { + // ................ 省略细节代码N行 + + // 如果正在 view-change, 忽略这个 Pre-Prepare 消息 + if !instance.activeView { + return nil + } + + // 如果收到的 Pre-Prepare 发送 id 不是当前 view 的主节点,忽略这个 Pre-Prepare 消息(view 一定,主节点一定 pbftCore.primary 函数) + if instance.primary(instance.view) != preprep.ReplicaId { + return nil + } + + // 当收到的 view 与 pbft-core 的 view 不一致,或 n 不在 watermarks 间的话,丢弃这个消息 + if !instance.inWV(preprep.View, preprep.SequenceNumber) { + if preprep.SequenceNumber != instance.h && !instance.skipInProgress { + } else { + // This is perfectly normal + } + return nil + } + + // 当 n 大于 viewChangeSeqNo, 发送 viewChange 消息,返回 + if preprep.SequenceNumber > instance.viewChangeSeqNo { + instance.sendViewChange() + return nil + } + + // 当收到相同序列号的消息时,如果消息体不同,进行 view-change 否则,将收到的 prePrepare, digest 存入 cert 里 + cert := instance.getCert(preprep.View, preprep.SequenceNumber) + if cert.digest != "" && cert.digest != preprep.BatchDigest { + return nil + } + cert.prePrepare = preprep + cert.digest = preprep.BatchDigest + + // 当 reqBatchStore 不存在当前 PrePrepare 消息的摘要时,首先计算摘要,如果摘要与收到的摘要一致,将摘要记录到 reqBatchStore 与 outstandingReqBatches 中,并持久化该 reqBatch + if _, ok := instance.reqBatchStore[preprep.BatchDigest]; !ok && preprep.BatchDigest != "" { + digest := hash(preprep.GetRequestBatch()) + if digest != preprep.BatchDigest { + return nil + } + instance.reqBatchStore[digest] = preprep.GetRequestBatch() + instance.outstandingReqBatches[digest] = preprep.GetRequestBatch() + instance.persistRequestBatch(digest) + } + + instance.softStartTimer(instance.requestTimeout, fmt.Sprintf("new pre-prepare for request batch %s", preprep.BatchDigest)) + instance.nullRequestTimer.Stop() + + // 当前节点不是主节点并且这个 Pre-Prepare 消息之前没有发送 Prepare 消息的话,构造 prepare 消息: + if instance.primary(instance.view) != instance.id && instance.prePrepared(preprep.BatchDigest, preprep.View, preprep.SequenceNumber) && !cert.sentPrepare { + prep := &Prepare{ + View: preprep.View, + SequenceNumber: preprep.SequenceNumber, + BatchDigest: preprep.BatchDigest, + ReplicaId: instance.id, + } + cert.sentPrepare = true + instance.persistQSet() + // 自己调用 recvPrepare(相当于自己收到了 Prepare 消息) + instance.recvPrepare(prep) + // 广播prepare消息 + return instance.innerBroadcast(&Message{Payload: &Message_Prepare{Prepare: prep}}) + } + + return nil +} +``` + + + +#### 收到prepare消息的处理流程 + + +```go +// /fabric/consensus/pbft/pbft-core.go +func (instance *pbftCore) recvPrepare(prep *Prepare) error { + // ................ 省略细节代码N行 + + // 如果收到的是主节点的消息,丢弃(prepare 不可能由主节点发送) + if instance.primary(prep.View) == prep.ReplicaId { + return nil + } + + // 当收到的 view 与 pbft-core 的 view 不一致,或 n 不在 watermarks 间的话,丢弃这个消息 + if !instance.inWV(prep.View, prep.SequenceNumber) { + if prep.SequenceNumber != instance.h && !instance.skipInProgress { + } else { + } + return nil + } + + // 获取 cert, 如果收到同一节点同一 view 的同一序号消息,则退出 + cert := instance.getCert(prep.View, prep.SequenceNumber) + for _, prevPrep := range cert.prepare { + if prevPrep.ReplicaId == prep.ReplicaId { + return nil + } + } + // 将 prepare 放入 cert, 并持久化 pset + cert.prepare = append(cert.prepare, prep) + instance.persistPSet() + + // 发送commit消息 + return instance.maybeSendCommit(prep.BatchDigest, prep.View, prep.SequenceNumber) +} +``` + + + + + +#### 收到commit消息的处理流程 + + +```go +// /fabric/consensus/pbft/pbft-core.go + +func (instance *pbftCore) recvCommit(commit *Commit) error { + // ................ 省略细节代码N行 + + // 当收到的 view 与 pbft-core 的 view 不一致,或 n 不在 watermarks 间的话,丢弃这个消息 + if !instance.inWV(commit.View, commit.SequenceNumber) { + if commit.SequenceNumber != instance.h && !instance.skipInProgress { + } else { + } + return nil + } + + // 获取 cert, 如果收到同一节点同一 view 的同一序号存在该 commit 消息,丢弃这个消息 + cert := instance.getCert(commit.View, commit.SequenceNumber) + for _, prevCommit := range cert.commit { + if prevCommit.ReplicaId == commit.ReplicaId { + return nil + } + } + // 添加 commit 到 cert 里 + cert.commit = append(cert.commit, commit) + + // 看该 msg 是否满足 commited 函数,若满足,将先前放入的 outstandingReqBatches 删除执行未完成的 batch, 执行完后,如果序列号等于 viewChangeSeqNo,进行 view-change + if instance.committed(commit.BatchDigest, commit.View, commit.SequenceNumber) { + instance.stopTimer() + instance.lastNewViewTimeout = instance.newViewTimeout + delete(instance.outstandingReqBatches, commit.BatchDigest) + + // 执行未完成的 batch executeOutstanding() + instance.executeOutstanding() + + if commit.SequenceNumber == instance.viewChangeSeqNo { + instance.sendViewChange() + } + } + + return nil +} +``` diff --git a/consensus/imgs/eos.producer.jpg b/consensus/imgs/eos.producer.jpg new file mode 100644 index 0000000..bc620e1 Binary files /dev/null and b/consensus/imgs/eos.producer.jpg differ diff --git a/consensus/imgs/eth.mining.diagram.jpg b/consensus/imgs/eth.mining.diagram.jpg new file mode 100644 index 0000000..8d7c1e3 Binary files /dev/null and b/consensus/imgs/eth.mining.diagram.jpg differ diff --git a/consensus/imgs/fabric.pbft.engine.creation.flow.jpg b/consensus/imgs/fabric.pbft.engine.creation.flow.jpg new file mode 100644 index 0000000..67e560d Binary files /dev/null and b/consensus/imgs/fabric.pbft.engine.creation.flow.jpg differ diff --git a/consensus/imgs/paxos.accept.jpeg b/consensus/imgs/paxos.accept.jpeg new file mode 100644 index 0000000..93dc316 Binary files /dev/null and b/consensus/imgs/paxos.accept.jpeg differ diff --git a/consensus/imgs/paxos.prepare.jpeg b/consensus/imgs/paxos.prepare.jpeg new file mode 100644 index 0000000..0e25d60 Binary files /dev/null and b/consensus/imgs/paxos.prepare.jpeg differ diff --git a/consensus/imgs/paxos.roles.jpeg b/consensus/imgs/paxos.roles.jpeg new file mode 100644 index 0000000..e48fd34 Binary files /dev/null and b/consensus/imgs/paxos.roles.jpeg differ diff --git a/consensus/imgs/raft.log.entries.png b/consensus/imgs/raft.log.entries.png new file mode 100644 index 0000000..e5ff5df Binary files /dev/null and b/consensus/imgs/raft.log.entries.png differ diff --git a/consensus/imgs/raft.log.structure.png b/consensus/imgs/raft.log.structure.png new file mode 100644 index 0000000..5c5f496 Binary files /dev/null and b/consensus/imgs/raft.log.structure.png differ diff --git a/consensus/imgs/raft.state.switch.png b/consensus/imgs/raft.state.switch.png new file mode 100644 index 0000000..a0449b5 Binary files /dev/null and b/consensus/imgs/raft.state.switch.png differ diff --git a/consensus/imgs/raft.terms.png b/consensus/imgs/raft.terms.png new file mode 100644 index 0000000..0d37bf3 Binary files /dev/null and b/consensus/imgs/raft.terms.png differ diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" new file mode 100644 index 0000000..ddb9302 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" @@ -0,0 +1,117 @@ +# cosmos源码分析之一概述 + +## 一、cosmos简介 +
+cosmos这个项目是为了实现跨链而生的,在众多的跨链项目中,cosmos有它自己独特的特点,它把共识和P2P协议抽象出来,形成了一个单独的Tendermint的层或者叫模块。它采用Tendermint-BFT DPoS的共识引擎来解决跨链的共识问题。 +
+众多的区块链项目,可以通过跨区块链沟通协议(IBC)来实现不同空间(cosmos用空间这个概念来表述不同的区块链)的信息传递。 +
+ +![tendermint](img/tendermint.jpg) +
+N多的应用可以通过ABCI这个回调接口协议来实现与Tendermint的交互。在他们的官网上实现了一个Ethermint,其实就是以太坊去除了POW的共识再加上Tendermint实现的一个基于POS的高效的新以太坊链。 +
+值得一提的是Cosmos-SDK,它是Cosmos团队的核心产品,其仍然是采用了ABCI应用的架构。(基于Go语言来实现)。如果使用这个SDK来开发一条自己的链,就不必过于纠结于链的底层的帐户体系,共识等技术细节,只需要关心自己的区块链的应用部分,而且在这个SDK中,还开放了一些开发的API,可以通过这些API来实现一些具体的基础实现,打造个性的区块链。 +
+ +![cosmosSDK](img/cosmos-sdk.jpg) +
+这也意味着,Cosmos-sdk是一个区块链开发的框架体系,通过Cosmos Hub接插任意的区块链,从理论上讲,万链并发不是幻想。cosmos的最终想法是通过cosmos SDK来实现一套完整的生态,但是正如波卡链和plasma一样,cosmos的进度确实是让人感到有些慢。很多的技术和概念可能会在未来不断变化。所以最终结果如何,还得走着瞧。 +
+ +## 二、cosmos的整体结构 + +### 1、网络和共识(Tendermint) +
+就如上面图上直观的表示一样,cosmos通过Tendermint实现了一个通用层,利用这个通用层,可以实现不同的应用的接插隔离。 +
+ +### 2、cosmos中心(Hub) +
+所谓中心,就是cosmos Hub,Cosmos网络中第一个公共区块链,通过Tendermint拜占庭共识算法运行。Cosmos中心通过ABCI来连接其他区块链(空间)。中心可以控制各个空间里代币。保证代币可以安全快速地从一个空间传递到另一个空间,即进行资产的跨链交易。 +
+Cosmos中心负责管理各个独立区块链(即“空间”,或者“碎片”)。中心上的空间会源源不断地提交最新区块,这一点可以让中心跟上每个空间状态的变化。 +
+Cosmos中心的验证人与委托人可以对提案进行投票,从而自动改变预先设置好的系统参数(比如区块容量限制),协调更新,并对人们看得懂的章程进行修订投票,从而管理Cosmos中心。这个章程允许权益相关者聚集到一起,来解决盗窃及漏洞等相关问题(比如The DAO事件),并快速得出明确的解决方案。 +
+ +### 3、空间(Zones) +
+空间,也就是各个独立的区块链,连接到cosmos中心后,它们就成为了cosmos的一个空间。每个空间也会和中心的状态保持一致。信息可以从一个空间发送到另外一个空间,通过布默克尔证明(Merkle-proof)来表明信息已经被传送或接收。这种机制叫做“区块链间通信”,简称为“IBC”机制。 +
+ +### 4、跨链通信协议(IBC) +
+IBC是中心与空间之前通信的方法。假设现在有三个区块链,分别是“空间1”、“空间2”以及“中心”,如果想要“空间1”生成一个消息包,通过“中心”发送给“空间2”。为了让消息包从一个区块链转移到另一个区块链,需要在接收方区块链上发布一个证明,来明确发送方已经发起了一个消息包到指定地点。接收方要验证的这个证明,必须和发送方区块头保持一致。这种机制就类似与侧链采用的机制,它需要两个相互作用的链,通过双向传送存在证明数据元(交易),来“知晓”另一方的情况。 +
+IBC协议可以自然定义为两种交易的使用:一种是IBCBlockCommitTx 交易,这种交易可以让区块链向任何观察员证明其最新区块哈希值;另一种是IBCPacketTx 交易,这种交易则可以证明某个消息包确实由发送者的应用程序,通过默克尔证明机制(Merkle-proof)传送到了最新区块的哈希值上。 +
+通过将IBC机制分裂成两个单独的交易,即IBCBlockCommitTx 交易与IBCPacketTx交易,可以让接收链的本地费用市场机制,来决定承认哪个消息包,与此同时还能确保发送方的完全自由,让其自行决定能够传出的消息包数量。 +
+
+ +## 三、项目分布 +
+ +cosmos可以分成cosmos(cosmos-SDK)和Tendermint两部分,它们在github上也是分成两个项目的。 +
+ +### 1、cosmos-SDK部分 +
+看一下文件: +
+ +![sdk](img/cosmosSDK-inc.png) +
+baseapp: 基本ABCI应用程序定义模板,以便Cosmos-SDK应用程序可以与底层的Tendermint节点通信。 +
+client:客户端 CLI和REST服务器工具,用于与SDK应用程序交互 +
+examples: 如何构建独立的应用程序的示例。 +
+server: 运行Tendermint上SDK应用程序的完整节点服务器。 +
+store: SDK的数据库-MyKLE多存储支持多种类型的Melkey密钥值存储。 +
+types: SDK应用程序中常见的类型。 +
+x: 对核心的扩展,其中定义了所有消息和处理程序。有点神秘的味道。 +
+ +### 2、Tendermint部分 +
+代码目录: +
+ +![tendermint](img/tendermint-inc.png) +
+blockchain:Tendermint链结构的规则验证和相关数据结构。 +
+state:状态管理跟踪,包括签名摘要以及最生要的默克尔证明等。 +
+consensus:共识部分,主要是基于拜占庭BFT的DPOS算法。 +
+mempool: 验证完成的交易的存储池。 +
+networks:本地和远端网络服务。 +
+node:区块节点及相关数据结构。类似于以太坊,抽象了一层。 +
+lite:轻节点,用来验证头。 +
+p2p:网络层,用于链的发现和治理。 +
+proxy:代理层,用于交易等代理连接验证共识等。 +
+evidence:存储存证相关。 +
+rpc:远程通信接口。 +
+types:基本的数据类型定义。 +
+
+ +## 四、总结 +
+Tendermint和波卡链的substrate类似,都是抽象出来用来处理跨链的一个模块。只不过一个倾向于中心接入,一个倾向于中继处理,异曲同工,达到的目的没有什么区别。 +
diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203Tendermint\346\236\266\346\236\204\345\210\206\346\236\220.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203Tendermint\346\236\266\346\236\204\345\210\206\346\236\220.md" new file mode 100644 index 0000000..1eb7798 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203Tendermint\346\236\266\346\236\204\345\210\206\346\236\220.md" @@ -0,0 +1,588 @@ +# cosmos源码分析之七Tendermint架构分析 + +## 一、介绍 +Tendermint在网络有人把它当成一个共识,有人把它当成一个通信组件。这都是可以理解的。Tendermint融合了共识和网络通信部分。它类似于一个软件包,通过使用Tendermint可以很容易的开发一个和Cosmos相兼容的区块链(当然,如果使用Cosmos-sdk会更简单,但是会屏蔽更多的细节)。可以把它理解成Cosmos的一个底层架构,提供类似于基础服务的一个平台。Tendermint可以提供一个Cosmos标准的跨链的基础应用。 +
+在前面分析完成Cosmos-sdk后,现在深入到网络通信和共识来看一看cosmos到底有什么特别之处? +
+ +## 二、整体架构 +## 1、BFT +Tendermint使用的共识算法是拜占庭容错共识协议,它是来源于DLS共识算法。使用这种算法的目的是可以简单方便的解决区块链的分叉机制。这种BFT的机制要求有固定的一组验证人,然后他们会尝试在某个区块上达成共识。每个区块的共识轮流进行,每一轮都有一个提议者来发起区块,之后由验证人来决定是否接受穿上区块或者进入下一轮投票。 +
+Tendermint采用由绝对多数的选票(三分之二)待定的最优拜占庭算法。因此它可以确定的做到: +
+如果想做恶,必须要有三分之一以上的选票出现问题,并且提交了两个值。 +
+如果任何验证组引起安全问题,就会被发现并对冲突进行投票,同时广播有问题的那些选票。 +
+因为使用了BFT,所以其共识的速度在所有的共识中最相当快速的,很容易达到并维持每秒千笔交易的速度。 +
+ +## 2、P2P +Tendermint中的网络底层通信,使用的是一种普通的反应器,它通过参数来查找需要连接的P2P节点,在Tendermint的节点连接中,维护着两组映射来管理连接自己和自己连接的对象,分别称做inbound,outbound. +
+outbound中,有两种连接,一种是连接时指定的seed,一种是在初始化时检测出来的节点。一般情况下,outbound的数量少于10个。而inbound控制在50个左右的连接。 +
+既然是基于反应器的,那么编程的复杂性就大大降低了。只需要服务监听就可以了。这里不再细节赘述网络通信部分。 +
+网络在启动时,会启动一个协和,定时轮询outbound的数量,来控制连接的稳定性。 +
+ +## 3、架构 +
+Tendermint的设计目的是为了创建一个统一的区块链开发的基础组件。通过将区块链中主要的P2P和共识抽象出来,实现区块链开发过程中的组件式管理。这样做的优势有以下几点: +
+一个是代码重用。对通用的网络通信和共识就不必再重复的造轮子。 +
+二是解放了区块链编程的语言。比如以太坊用go,c++,但是通过Tendermint的抽象后,可以使用任何语言(觉得和当初JAVA才提出时一次编译的想法有些相似啊)。特别是对于智能合约,这个优点就更显得明显了。 +
+ +## 4、Tendermint的共识过程 +Tendermint共识机制中通过作验证人(Validators)来对区块达成共识,这个在前面已经介绍过,一组验证人负责对每一轮的新区块进行提议和投票。整个共识达成的过程如下图所示。 +
+ +![gs](img/Consensus.png) +
+每一轮的开始(New Round),节点对新一轮的区块进行提议。之后,合格的提议区块首先经过一轮预投票(Prevote)。在提议区块获得2/3以上的投票后,进入下一轮的预认可(Precommit),同样是待获得2/3以上的验证人预认可后,被提议区块就正式获得了认可(Commit)。而得到认可的这个区块就被添加的到区块链中。 +
+下面为详细的过程: +
+![gs-0](img/consensus-0.jpg) +
+![gs-1](img/consensus-1.jpg) +
+在Tendermint算法中,如果遇到对同一特定区块的同意及否决信息同时超过2/3的情况,需要启用外部的维护机制去核查是否存在超过1/3的验证节点伪造签名或者投出双重选票。 +
+ +## 三、源码分析 + +## 1、共识部分 +共识中有一个重要的数据结构,那就是共识的状态,它决定的共识进行的步骤: +
+ + ``` golang + type ConsensusState struct { + cmn.BaseService + + // config details + config *cfg.ConsensusConfig + privValidator types.PrivValidator // for signing votes + + // services for creating and executing blocks + // TODO: encapsulate all of this in one "BlockManager" + blockExec *sm.BlockExecutor + blockStore types.BlockStore + mempool types.Mempool + evpool types.EvidencePool + + // internal state + mtx sync.Mutex + cstypes.RoundState + state sm.State // State until height-1. + + // state changes may be triggered by: msgs from peers, + // msgs from ourself, or by timeouts + peerMsgQueue chan msgInfo + internalMsgQueue chan msgInfo + timeoutTicker TimeoutTicker + + // we use eventBus to trigger msg broadcasts in the reactor, + // and to notify external subscribers, eg. through a websocket + eventBus *types.EventBus + + // a Write-Ahead Log ensures we can recover from any kind of crash + // and helps us avoid signing conflicting votes + wal WAL + replayMode bool // so we don't log signing errors during replay + doWALCatchup bool // determines if we even try to do the catchup + + // for tests where we want to limit the number of transitions the state makes + nSteps int + + // some functions can be overwritten for testing + decideProposal func(height int64, round int) + doPrevote func(height int64, round int) + setProposal func(proposal *types.Proposal) error + + // closed when we finish shutting down + done chan struct{} + + // synchronous pubsub between consensus state and reactor. + // state only emits EventNewRoundStep, EventVote and EventProposalHeartbeat + evsw tmevents.EventSwitch + } + ``` +
+所以在共识要首先创建一个Proposal: +
+ +``` golang +func (cs *ConsensusState) createProposalBlock() (block *types.Block, blockParts *types.PartSet) { + var commit *types.Commit + if cs.Height == 1 { + // We're creating a proposal for the first block. + // The commit is empty, but not nil. + commit = &types.Commit{} + } else if cs.LastCommit.HasTwoThirdsMajority() { + // Make the commit from LastCommit + commit = cs.LastCommit.MakeCommit() + } else { + // This shouldn't happen. + cs.Logger.Error("enterPropose: Cannot propose anything: No commit for the previous block.") + return + } + + // Mempool validated transactions + txs := cs.mempool.Reap(cs.config.MaxBlockSizeTxs) + block, parts := cs.state.MakeBlock(cs.Height, txs, commit) + evidence := cs.evpool.PendingEvidence() + block.AddEvidence(evidence) + return block, parts +} +func NewProposal(height int64, round int, blockPartsHeader PartSetHeader, polRound int, polBlockID BlockID) *Proposal { + return &Proposal{ + Height: height, + Round: round, + Timestamp: time.Now().UTC(), + BlockPartsHeader: blockPartsHeader, + POLRound: polRound, + POLBlockID: polBlockID, + } +} +``` +
+ 然后进行验证: +
+ +``` golang +// Implements PrivValidator. +func (pv *MockPV) SignProposal(chainID string, proposal *Proposal) error { + signBytes := proposal.SignBytes(chainID) + sig := pv.privKey.Sign(signBytes) + proposal.Signature = sig + return nil +} +``` +
+广播: +
+ +``` golang +type peer struct { + cmn.BaseService + + // raw peerConn and the multiplex connection + peerConn + mconn *tmconn.MConnection + + // peer's node info and the channel it knows about + // channels = nodeInfo.Channels + // cached to avoid copying nodeInfo in hasChannel + nodeInfo NodeInfo + channels []byte + + // User data + Data *cmn.CMap +} +func (p *peer) Send(chID byte, msgBytes []byte) bool { + if !p.IsRunning() { + // see Switch#Broadcast, where we fetch the list of peers and loop over + // them - while we're looping, one peer may be removed and stopped. + return false + } else if !p.hasChannel(chID) { + return false + } + return p.mconn.Send(chID, msgBytes) +} +``` +
+真正的共识算法过程在state中: +
+ +``` golang +type RoundState struct { + Height int64 `json:"height"` // Height we are working on + Round int `json:"round"` + Step RoundStepType `json:"step"` + StartTime time.Time `json:"start_time"` + CommitTime time.Time `json:"commit_time"` // Subjective time when +2/3 precommits for Block at Round were found + Validators *types.ValidatorSet `json:"validators"` + Proposal *types.Proposal `json:"proposal"` + ProposalBlock *types.Block `json:"proposal_block"` + ProposalBlockParts *types.PartSet `json:"proposal_block_parts"` + LockedRound int `json:"locked_round"` + LockedBlock *types.Block `json:"locked_block"` + LockedBlockParts *types.PartSet `json:"locked_block_parts"` + ValidRound int `json:"valid_round"` // Last known round with POL for non-nil valid block. + ValidBlock *types.Block `json:"valid_block"` // Last known block of POL mentioned above. + ValidBlockParts *types.PartSet `json:"valid_block_parts"` // Last known block parts of POL metnioned above. + Votes *HeightVoteSet `json:"votes"` + CommitRound int `json:"commit_round"` // + LastCommit *types.VoteSet `json:"last_commit"` // Last precommits at Height-1 + LastValidators *types.ValidatorSet `json:"last_validators"` +} +func (hvs *HeightVoteSet) POLInfo() (polRound int, polBlockID types.BlockID) { + hvs.mtx.Lock() + defer hvs.mtx.Unlock() + for r := hvs.round; r >= 0; r-- { + rvs := hvs.getVoteSet(r, types.VoteTypePrevote) + polBlockID, ok := rvs.TwoThirdsMajority() + if ok { + return r, polBlockID + } + } + return -1, types.BlockID{} +} +//启动一轮新的共识 +func (cs *ConsensusState) startRoutines(maxSteps int) { + err := cs.timeoutTicker.Start() + if err != nil { + cs.Logger.Error("Error starting timeout ticker", "err", err) + return + } + go cs.receiveRoutine(maxSteps) +} +// Updates ConsensusState and increments height to match that of state. +// The round becomes 0 and cs.Step becomes cstypes.RoundStepNewHeight. +func (cs *ConsensusState) updateToState(state sm.State) { + if cs.CommitRound > -1 && 0 < cs.Height && cs.Height != state.LastBlockHeight { + cmn.PanicSanity(cmn.Fmt("updateToState() expected state height of %v but found %v", + cs.Height, state.LastBlockHeight)) + } + if !cs.state.IsEmpty() && cs.state.LastBlockHeight+1 != cs.Height { + // This might happen when someone else is mutating cs.state. + // Someone forgot to pass in state.Copy() somewhere?! + cmn.PanicSanity(cmn.Fmt("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v", + cs.state.LastBlockHeight+1, cs.Height)) + } + + // If state isn't further out than cs.state, just ignore. + // This happens when SwitchToConsensus() is called in the reactor. + // We don't want to reset e.g. the Votes. + if !cs.state.IsEmpty() && (state.LastBlockHeight <= cs.state.LastBlockHeight) { + cs.Logger.Info("Ignoring updateToState()", "newHeight", state.LastBlockHeight+1, "oldHeight", cs.state.LastBlockHeight+1) + return + } + + // Reset fields based on state. + validators := state.Validators + lastPrecommits := (*types.VoteSet)(nil) + if cs.CommitRound > -1 && cs.Votes != nil { + if !cs.Votes.Precommits(cs.CommitRound).HasTwoThirdsMajority() { + cmn.PanicSanity("updateToState(state) called but last Precommit round didn't have +2/3") + } + lastPrecommits = cs.Votes.Precommits(cs.CommitRound) + } + + // Next desired block height + height := state.LastBlockHeight + 1 + + // RoundState fields + cs.updateHeight(height) + cs.updateRoundStep(0, cstypes.RoundStepNewHeight) + if cs.CommitTime.IsZero() { + // "Now" makes it easier to sync up dev nodes. + // We add timeoutCommit to allow transactions + // to be gathered for the first block. + // And alternative solution that relies on clocks: + // cs.StartTime = state.LastBlockTime.Add(timeoutCommit) + cs.StartTime = cs.config.Commit(time.Now()) + } else { + cs.StartTime = cs.config.Commit(cs.CommitTime) + } + cs.Validators = validators + cs.Proposal = nil + cs.ProposalBlock = nil + cs.ProposalBlockParts = nil + cs.LockedRound = 0 + cs.LockedBlock = nil + cs.LockedBlockParts = nil + cs.ValidRound = 0 + cs.ValidBlock = nil + cs.ValidBlockParts = nil + cs.Votes = cstypes.NewHeightVoteSet(state.ChainID, height, validators) + cs.CommitRound = -1 + cs.LastCommit = lastPrecommits + cs.LastValidators = state.LastValidators + + cs.state = state + + // Finally, broadcast RoundState + cs.newStep() +} + +``` +
+细节之处其实就是BFT的三阶段提交,对着代码就可以看得很清楚。 +
+ +## 3、P2P部分 +首先看一下连接的数据结构体: +
+ +``` golang +type MConnection struct { + cmn.BaseService + + conn net.Conn + bufConnReader *bufio.Reader + bufConnWriter *bufio.Writer + sendMonitor *flow.Monitor + recvMonitor *flow.Monitor + send chan struct{} + pong chan struct{} + channels []*Channel + channelsIdx map[byte]*Channel + onReceive receiveCbFunc + onError errorCbFunc + errored uint32 + config *MConnConfig + + quit chan struct{} + flushTimer *cmn.ThrottleTimer // flush writes as necessary but throttled. + pingTimer *cmn.RepeatTimer // send pings periodically + + // close conn if pong is not received in pongTimeout + pongTimer *time.Timer + pongTimeoutCh chan bool // true - timeout, false - peer sent pong + + chStatsTimer *cmn.RepeatTimer // update channel stats periodically + + created time.Time // time of creation +} +``` +
+其中的代码还是比较容易看清楚的,每一个Peer需要有一个这个结构体的变量的实例。它可以发送数据: +
+ +``` golang +// Queues a message to be sent to channel. +func (c *MConnection) Send(chID byte, msgBytes []byte) bool { + if !c.IsRunning() { + return false + } + + c.Logger.Debug("Send", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes)) + + // Send message to channel. + channel, ok := c.channelsIdx[chID] + if !ok { + c.Logger.Error(cmn.Fmt("Cannot send bytes, unknown channel %X", chID)) + return false + } + + success := channel.sendBytes(msgBytes) + if success { + // Wake up sendRoutine if necessary + select { + case c.send <- struct{}{}: + default: + } + } else { + c.Logger.Error("Send failed", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes)) + } + return success +} +``` +
+当然,它还有很多功能,这里就不再都列举出来。下面看一下Peer的代码: +
+ +``` golang +type Peer interface { + cmn.Service + + ID() ID // peer's cryptographic ID + RemoteIP() net.IP // remote IP of the connection + IsOutbound() bool // did we dial the peer + IsPersistent() bool // do we redial this peer when we disconnect + NodeInfo() NodeInfo // peer's info + Status() tmconn.ConnectionStatus + + Send(byte, []byte) bool + TrySend(byte, []byte) bool + + Set(string, interface{}) + Get(string) interface{} +} + +//---------------------------------------------------------- + +// peerConn contains the raw connection and its config. +type peerConn struct { + outbound bool + persistent bool + config *PeerConfig + conn net.Conn // source connection + ip net.IP +} +``` +
+它的两类连接: +
+ +``` golang +func newOutboundPeerConn(addr *NetAddress, config *PeerConfig, persistent bool, ourNodePrivKey crypto.PrivKey) (peerConn, error) { + var pc peerConn + + conn, err := dial(addr, config) + if err != nil { + return pc, cmn.ErrorWrap(err, "Error creating peer") + } + + pc, err = newPeerConn(conn, config, true, persistent, ourNodePrivKey) + if err != nil { + if err2 := conn.Close(); err2 != nil { + return pc, cmn.ErrorWrap(err, err2.Error()) + } + return pc, err + } + + // ensure dialed ID matches connection ID + if addr.ID != pc.ID() { + if err2 := conn.Close(); err2 != nil { + return pc, cmn.ErrorWrap(err, err2.Error()) + } + return pc, ErrSwitchAuthenticationFailure{addr, pc.ID()} + } + return pc, nil +} + +func newInboundPeerConn(conn net.Conn, config *PeerConfig, ourNodePrivKey crypto.PrivKey) (peerConn, error) { + + // TODO: issue PoW challenge + + return newPeerConn(conn, config, false, false, ourNodePrivKey) +} + +func newPeerConn(rawConn net.Conn, + config *PeerConfig, outbound, persistent bool, + ourNodePrivKey crypto.PrivKey) (pc peerConn, err error) { + + conn := rawConn + + // Fuzz connection + if config.Fuzz { + // so we have time to do peer handshakes and get set up + conn = FuzzConnAfterFromConfig(conn, 10*time.Second, config.FuzzConfig) + } + + // Set deadline for secret handshake + if err := conn.SetDeadline(time.Now().Add(config.HandshakeTimeout * time.Second)); err != nil { + return pc, cmn.ErrorWrap(err, "Error setting deadline while encrypting connection") + } + + // Encrypt connection + conn, err = tmconn.MakeSecretConnection(conn, ourNodePrivKey) + if err != nil { + return pc, cmn.ErrorWrap(err, "Error creating peer") + } + + // Only the information we already have + return peerConn{ + config: config, + outbound: outbound, + persistent: persistent, + conn: conn, + }, nil +} +``` +
+它们由一个公共的接口来控制: +
+ +``` golang +type Reactor interface { + cmn.Service // Start, Stop + + // SetSwitch allows setting a switch. + SetSwitch(*Switch) + + // GetChannels returns the list of channel descriptors. + GetChannels() []*conn.ChannelDescriptor + + // AddPeer is called by the switch when a new peer is added. + AddPeer(peer Peer) + + // RemovePeer is called by the switch when the peer is stopped (due to error + // or other reason). + RemovePeer(peer Peer, reason interface{}) + + // Receive is called when msgBytes is received from peer. + // + // NOTE reactor can not keep msgBytes around after Receive completes without + // copying. + // + // CONTRACT: msgBytes are not nil. + Receive(chID byte, peer Peer, msgBytes []byte) +} + +//-------------------------------------- + +type BaseReactor struct { + cmn.BaseService // Provides Start, Stop, .Quit + Switch *Switch +} +``` +
+节点的管理和控制都要通过这个接口继承的相关的结构体来处理。 +
+服务器的监听由listener.go来实现: +
+ +``` golang +type Listener interface { + Connections() <-chan net.Conn + InternalAddress() *NetAddress + ExternalAddress() *NetAddress + String() string + Stop() error +} + +// Implements Listener +type DefaultListener struct { + cmn.BaseService + + listener net.Listener + intAddr *NetAddress + extAddr *NetAddress + connections chan net.Conn +} +``` +
+启动监听: +
+ +``` golang +// Accept connections and pass on the channel +func (l *DefaultListener) listenRoutine() { + for { + conn, err := l.listener.Accept() + + if !l.IsRunning() { + break // Go to cleanup + } + + // listener wasn't stopped, + // yet we encountered an error. + if err != nil { + panic(err) + } + + l.connections <- conn + } + + // Cleanup + close(l.connections) + for range l.connections { + // Drain + } +} +``` +
+总体上来讲,这波代码写得中规中矩,没有什么出彩的地方。估计Tendermint是搂草打兔子,重点还是在共识和通信协议的处理以及对整个模块的封装上。 +
+ +## 四、总结 +通过上面的分析可以看到,其实Tendermint的重点在于共识和P2P,相关的通信的协议和接口在前面介绍过。将二者抽象出来的有利之处在于,可以让开发者忽略对网络通信和共识的复杂性。直接进行业务层面的开发,而在前面也提到了SDK的封装,这进一步减少了业务上对非相关的逻辑的考虑,大大减少了开发者生产一条区块链的复杂度,而这也恰恰是Tendermint和cosmos-sdk所想达到的目的。 diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\350\267\250\351\223\276\344\272\244\346\230\223.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\350\267\250\351\223\276\344\272\244\346\230\223.md" new file mode 100644 index 0000000..5af6eb2 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\350\267\250\351\223\276\344\272\244\346\230\223.md" @@ -0,0 +1,278 @@ +# cosmos源码分析之三跨链交易 +cosmos中最重的一部分就是跨链通信,在上文提到过,跨链是通过IBC协议来实现的.cosmos中交易分为两种:普通交易和跨链交易。普通交易基本局限在各个区块链的交易方式中,在Basecoin中已经提供了基础的转帐功能供使用;跨链交易主要由IBC协议来实现。 +
+ +## 一、普通交易 +cosmos的普通交易和以太坊类似,也是一个帐户模型。但是与以太坊不同的是,cosmos的交易进行了插件化的处理,也就是说,最基础最简单的交易可以使用Basecoin提供的基础函数,但是要想实现以太坊一样的丰富的交易管理就得使用交易的插件,这也是在上文中提到的Cosmos基本实现了插件化的编程。 +
+ +``` golang +// SendTxCommand will create a send tx and sign it with the given key +func SendTxCmd(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "send", + Short: "Create and sign a send tx", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) +....... + + // build and sign the transaction, then broadcast to Tendermint + msg := client.BuildMsg(from, to, coins) + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) +...... + }, + } + +...... + return cmd +} +// build the sendTx msg +func BuildMsg(from sdk.Address, to sdk.Address, coins sdk.Coins) sdk.Msg { + input := bank.NewInput(from, coins) + output := bank.NewOutput(to, coins) + msg := bank.NewMsgSend([]bank.Input{input}, []bank.Output{output}) + return msg +} +// NewInput - create a transaction input, used with MsgSend +func NewInput(addr sdk.Address, coins sdk.Coins) Input { + input := Input{ + Address: addr, + Coins: coins, + } + return input +} +// NewOutput - create a transaction output, used with MsgSend +func NewOutput(addr sdk.Address, coins sdk.Coins) Output { + output := Output{ + Address: addr, + Coins: coins, + } + return output +} + +``` +
+普通交易比较简单,不是本次分析的重点。 +
+ +## 二、跨链交易 + +### 1、交易步骤 +
+IBC协议中包含了两个主要的交易类型: +
+IBCBlockCommitTx:发送方所在区块链的最新的区块信息。 +
+IBCPacketTx:跨链交易本身的信息,及其在发送方链中被打包的区块信息。 +
+其中IBCBlockCommitTx就是传送MerkleRoot到另外一条链的消息。而IBCPacketTx则传递了跨链的交易信息。也就是交易经常提到的有效载荷(payload)。而为了保证数据的安全,双方链需要创建两个队列,一进一出,用来存储入链(从对方链进来的消息)和出链(发给对方链的消息)。 +
+当双方在路由注册后,彼此发现就可以通过路由进行跨链交易了。(下图来自美图的Cosmos分析) +
+ +![meitu](img/meitu-cross.jpg) +
+它主要有以下几个步骤: +
+1、路由注册。 +
+2、User A向chain1提交跨链交易(包括验证信息等),chain1执行该交易,并将交易存入outgoing队列。 +
+3、relayer从chain1中对应test-hub的outgoing消息队列取出跨链交易,提交到test-hub。 +
+4、test-hub执行交易,放入incoming消息队列。 +
+此的流程和官网的白皮书略有不同,和前面交易的步骤中提到的也有区别,主要原因是代码尚未完成,按它们的计划需要明年第三季度才可能完成。所以细节有出入是正常的。 +
+ +### 2、源码分析 +在看了上面的分析之后,结合代码分析一下: +
+1、注册路由 +
+ +``` golang +func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { +...... + // add accountMapper/handlers + app.coinKeeper = bank.NewKeeper(app.accountMapper) + app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace)) + app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace)) + app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace)) + + // register message routes + app.Router(). + AddRoute("auth", auth.NewHandler(app.accountMapper)). + AddRoute("bank", bank.NewHandler(app.coinKeeper)). + AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)). + AddRoute("stake", stake.NewHandler(app.stakeKeeper)) + +....... + return app +} +``` +
+2、发起传送 +
+ +``` golang +// SendTxCommand will create a send tx and sign it with the given key +func SendTxCmd(cdc *wire.Codec) *cobra.Command { +...... + + // build and sign the transaction, then broadcast to Tendermint + msg := client.BuildMsg(from, to, coins) + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) +...... + return cmd +} +// IBC transfer command +func IBCTransferCmd(cdc *wire.Codec) *cobra.Command { +....... + + // build the message + msg, err := buildMsg(from) + if err != nil { + return err + } + + // get password + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) + if err != nil { + return err + } + +....... + return cmd +} +``` +
+3、Relay传输 +
+ +``` golang +// IBC relay command +func IBCRelayCmd(cdc *wire.Codec) *cobra.Command { +...... + + cmd := &cobra.Command{ + Use: "relay", + Run: cmdr.runIBCRelay, + } +....... + + return cmd +} + +func (c relayCommander) runIBCRelay(cmd *cobra.Command, args []string) { + //得到路由的相关信息 + fromChainID := viper.GetString(FlagFromChainID) + fromChainNode := viper.GetString(FlagFromChainNode) + toChainID := viper.GetString(FlagToChainID) + toChainNode := viper.GetString(FlagToChainNode) + address, err := context.NewCoreContextFromViper().GetFromAddress() + if err != nil { + panic(err) + } + c.address = address + + //循环处理路由 + c.loop(fromChainID, fromChainNode, toChainID, toChainNode) +} + +func (c relayCommander) loop(fromChainID, fromChainNode, toChainID, + toChainNode string) { + + ctx := context.NewCoreContextFromViper() + // get password + passphrase, err := ctx.GetPassphraseFromStdin(ctx.FromAddressName) + if err != nil { + panic(err) + } + + ingressKey := ibc.IngressSequenceKey(fromChainID) + +OUTER: + for { + time.Sleep(5 * time.Second) + + processedbz, err := query(toChainNode, ingressKey, c.ibcStore) + if err != nil { + panic(err) + } + + var processed int64 + if processedbz == nil { + processed = 0 + } else if err = c.cdc.UnmarshalBinary(processedbz, &processed); err != nil { + panic(err) + } + + lengthKey := ibc.EgressLengthKey(toChainID) + egressLengthbz, err := query(fromChainNode, lengthKey, c.ibcStore) + if err != nil { + c.logger.Error("Error querying outgoing packet list length", "err", err) + continue OUTER //TODO replace with continue (I think it should just to the correct place where OUTER is now) + } + var egressLength int64 + if egressLengthbz == nil { + egressLength = 0 + } else if err = c.cdc.UnmarshalBinary(egressLengthbz, &egressLength); err != nil { + panic(err) + } + if egressLength > processed { + c.logger.Info("Detected IBC packet", "number", egressLength-1) + } + + seq := c.getSequence(toChainNode) + + for i := processed; i < egressLength; i++ { + egressbz, err := query(fromChainNode, ibc.EgressKey(toChainID, i), c.ibcStore) + if err != nil { + c.logger.Error("Error querying egress packet", "err", err) + continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER) + } + //在此广播交易 + err = c.broadcastTx(seq, toChainNode, c.refine(egressbz, i, passphrase)) + seq++ + if err != nil { + c.logger.Error("Error broadcasting ingress packet", "err", err) + continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER) + } + + c.logger.Info("Relayed IBC packet", "number", i) + } + } +} +``` +
+4、执行交易 +
+ +``` golang +// IBCReceiveMsg adds coins to the destination address and creates an ingress IBC packet. +func handleIBCReceiveMsg(ctx sdk.Context, ibcm Mapper, ck bank.Keeper, msg IBCReceiveMsg) sdk.Result { +...... + _, _, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins) +...... + + return sdk.Result{} +} +``` +
+5、相关的数据结构体 +
+ +``` golang +// MsgSend - high level transaction of the coin module +type MsgSend struct { + Inputs []Input `json:"inputs"` + Outputs []Output `json:"outputs"` +} +``` +
+ +## 三、总结 +通过上述的分析,可以看到,其实整个Cosmos还是处于一个非常初级的阶段,大量的相关代码都处理演进状态。所以说白皮书写得好,还得看代码进度。 +
+不过从交易的设计角度来看,这还是有一些值得借鉴的。这里没有讨论已存在的链(如比特币,以太坊等)在Cosmos-sdk上的交易的办法,解决的方式PegZone部分以后再专门进行分析。 diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214cosmos-SDK\346\265\201\347\250\213.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214cosmos-SDK\346\265\201\347\250\213.md" new file mode 100644 index 0000000..8c53564 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214cosmos-SDK\346\265\201\347\250\213.md" @@ -0,0 +1,666 @@ +# cosmos源码分析之二整体流程 + +cosmos主要的源码其实是在SDK部分,听名字也可以理解出来,直接用这个SDK就可以写出一条不考虑底层的区块链来,但是,做为中继链的一个代表,理想和现实并不是那么完美的结合在一起。 +
+目前区块链的跨链的难点在于,网络异构、共识算法不兼容等,而解决这些问题,都意味着巨大的投入和风险。Cosmos的目的当然是想建立一个区块链互联网,所以他的想法是把网络和共识抽象出来,专门做了一层,这在上一篇也讲过了。但是,这样做的方法,虽然从理论上讲是没有问题的,可开发上,难度还是增加了,开发者必须适应新的套路和不同的设计方法,怎么办? +
+凉拌,弄个SDK,隔离变化,软件界的通用手段。 +
+ +## 一、SDK的架构 + +看一个架构图: +
+ +![sdk](img/owner-sdk.png) +
+从上图可以看出来,其实SDK就是为了APP服务的,图中的应用程序其实就是给的例子,让大家能快速上手。然后另外两部分一个是和抽象层(共识和P2P)通信的,另外一个是用来调用各种插件的。 +
+SDK从开始到现在,也进行了好几次比较大的改动了,至于今后会不会再有大的改动,也不敢肯定,所以说,做成插件化,是一个最好的选择,到时候儿看谁不顺眼,直接搞掉就可以了,喜欢谁,把插件接进来就OK。 +
+
+ +### 1、plugins层 +
+在插件层其实图中画的并是很完全只是一个示意。主要的几个插件包括staking、IBC、 bank、 auth、 governance 、tx、 keys等几个。staking主要是控制Atom持有者相关贡献。类似一个汇率机制,动态变化。IBC其实就是链间通信机制,因为各个通信链是通过插件插入到HUB中进行通信,所以需要一个相应的通信机制来保证通信的安全性。governance这个模块目前在源码中看好像注释了不少,只保留了较少的东西,它主要是治理相关的实现,如提议、投票等。bank其实就是提供了一系列的通信接口(资产转移的),所以叫“银行”。 +
+ +### 2、APP层 +
+这一层基本没啥可说的,应该就是客户开发的APP,但是为了能让客户迅速进入,提供了三个相关的Demo。其中Basecoin是第一个完成的,是一个相对完整的应用,实现了SDK的核心模块的扩展,提供了诸如帐户管理、管理交易类型、处理存储等。 +
+其它两个都是相关的扩展。 +
+ +### 3、BaseApp +
+这一层主要是ABCI的通信,和Tendermint进行交互,这个说过好几次了,Cosmos的核心就在这里。 +
+ +## 二、源码流程 + +### 1、启动流程 +从主程序的接口来分析源码: +
+ +``` golang +//gaia/cmd/gaiad/main.go +func main() { + //生成需要注册的编解码器 + cdc := app.MakeCodec() + ctx := server.NewDefaultContext() + cobra.EnableCommandSorting = false + rootCmd := &cobra.Command{ + Use: "gaiad", + Short: "Gaia Daemon (server)", + PersistentPreRunE: server.PersistentPreRunEFn(ctx), + } + + server.AddCommands(ctx, cdc, rootCmd, app.GaiaAppInit(), + //第一步,创建APP,在其中准备启动网络的各个条件 + server.ConstructAppCreator(newApp, "gaia"), + //第二步,导出相生成的创世文件相关状态 + server.ConstructAppExporter(exportAppState, "gaia")) + + // 第三步,prepare and add flags设置并启动Tendermint + executor := cli.PrepareBaseCmd(rootCmd, "GA", app.DefaultNodeHome) + executor.Execute() +} + +func newApp(logger log.Logger, db dbm.DB) abci.Application { + return app.NewGaiaApp(logger, db) +} + +func exportAppState(logger log.Logger, db dbm.DB) (json.RawMessage, error) { + gapp := app.NewGaiaApp(logger, db) + return gapp.ExportAppStateJSON() +} +``` +
+这里只分析前两步,最后一步等分析Tendermint时再展开分析。 +
+ +``` golang +func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp { + cdc := MakeCodec() + + // create your application object + //创建一个相关的APP,其它所有的APP都可以按照这个方法 + var app = &GaiaApp{ + BaseApp: bam.NewBaseApp(appName, cdc, logger, db), + cdc: cdc, + keyMain: sdk.NewKVStoreKey("main"), + keyAccount: sdk.NewKVStoreKey("acc"), + keyIBC: sdk.NewKVStoreKey("ibc"), + keyStake: sdk.NewKVStoreKey("stake"), + keySlashing: sdk.NewKVStoreKey("slashing"), + } + + // define the accountMapper + //帐户管理--从KVSTROE抽象 + app.accountMapper = auth.NewAccountMapper( + app.cdc, + app.keyAccount, // target store + &auth.BaseAccount{}, // prototype + ) + + // add handlers + //添加各种操作——它们都从KVSTORE抽象出来,但是它们的抽象度更高,或者可以认为是accountMapper的更高一层。 + //处理帐户的操作,再抽象一层 + app.coinKeeper = bank.NewKeeper(app.accountMapper) + app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace)) + //处理Atom + app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace)) + //设置惩罚机制操作者 + app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace)) + + // register message routes + //这个是重点,在这里注册路由的句柄 + app.Router(). + AddRoute("bank", bank.NewHandler(app.coinKeeper)). + AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)). + AddRoute("stake", stake.NewHandler(app.stakeKeeper)) + + // initialize BaseApp + //初始化相关参数 + app.SetInitChainer(app.initChainer) + app.SetBeginBlocker(app.BeginBlocker) + app.SetEndBlocker(app.EndBlocker) + //设置权限控制句柄 + app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper)) + //从KV数据库加载相关数据--在当前版本中,IVAL存储是KVStore基础的实现 + app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake, app.keySlashing) + err := app.LoadLatestVersion(app.keyMain) + if err != nil { + cmn.Exit(err.Error()) + } + + return app +} + +// custom tx codec +//将相关的编码器注册到相关的各方 +func MakeCodec() *wire.Codec { + var cdc = wire.NewCodec() + ibc.RegisterWire(cdc) + bank.RegisterWire(cdc) + stake.RegisterWire(cdc) + slashing.RegisterWire(cdc) + auth.RegisterWire(cdc) + sdk.RegisterWire(cdc) + wire.RegisterCrypto(cdc) + return cdc +} +//其下为具体的上面的HANDLER的设置 +// application updates every end block +func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + tags := slashing.BeginBlocker(ctx, req, app.slashingKeeper) + + return abci.ResponseBeginBlock{ + Tags: tags.ToKVPairs(), + } +} + +// application updates every end block +func (app *GaiaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper) + + return abci.ResponseEndBlock{ + ValidatorUpdates: validatorUpdates, + } +} + +// custom logic for gaia initialization +func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + stateJSON := req.AppStateBytes + // TODO is this now the whole genesis file? + + var genesisState GenesisState + err := app.cdc.UnmarshalJSON(stateJSON, &genesisState) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + // load the accounts + for _, gacc := range genesisState.Accounts { + acc := gacc.ToAccount() + app.accountMapper.SetAccount(ctx, acc) + } + + // load the initial stake information + stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData) + + return abci.ResponseInitChain{} +} + +``` +
+这里面需要说明的是,Mapper和Keeper。记得在写数据库程序的时候儿,有几种方法,一种是直接连接操作数据库,拿到结果,这种方法最原始,但是权力也最大,想怎么操作就怎么操作。后来有了可以使用封装对象,这样访问数据库就被控制了起来,但是仍然是可以访问很多原始的东西。现在主流的使用的是Mybaits什么的,抽象的更厉害,基本上与你无关的数据,你根本不知道在哪儿了。 +
+Mapper和Keeper就是干这个的,前者抽象度一般,后者更高一些。目的就是限制模块对功能访问的方式。按照最小权限原则来提供访问机制。这样,安全性和不必要的异常的出现就被控制起来,使得应用上更容易扩展。 +
+这里其实主要是governance和slashing这两上解释的不太多,前者主要是控制提议和投票等,后者主要是防止有人做恶,然后从staking中slash掉你的Atom。说白了就是把你的抵押的钱没收。这里顺道说一下这个原则:Atom的持有者可以是验证人也可以是委托人,委托人可以根据他们对验证人的认知和具体的情况将币委托给验证人,验证人即可代理Atom资产并从每个出块奖励中得到大部分,另外有一小部分给委托人,还有一小部分供节点的自运行。而为了保证验证人的诚实,向区块链中发布不正确的数据的恶意验证人会失去他们的Atom。这就叫做slashing。 +
+ +### 2、ABCI接口分析 +在整个的SDK的流程中,调用ABCI同Tendermint进行通信是一个重要的机制。虽然这篇并不讨论Tendermint,但是相关的ABCI的接口得说明一下,否则在SDK的流程调用中不明白相关的规则,就会导致对整个流程的理解无法正常进行。ABCI有三种消息类型,DeliverTx,CheckTx, Commit。其中DeliverTx和BeginBlock和EndBlock两个接口有关系。 +
+1、InitChain +
+在上面的流程介绍过有app.initChain的方法,它会被Tendermint在启动时调用一次,用来初始化各种相关的Message,比如共识层的参数和最初的验证人的集合数据。当然,肯定还会有决定信息处理的方式。在白皮书中提到,你可以在此处将引进的数据结构进行JSON编码,然后在调用这个函数的期间,对这些信息进行填充并存储。 +
+ +``` golang +// Implements ABCI +// InitChain runs the initialization logic directly on the CommitMultiStore and commits it. +func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) { + if app.initChainer == nil { + return + } + + // Initialize the deliver state and run initChain + app.setDeliverState(abci.Header{}) + app.initChainer(app.deliverState.ctx, req) // no error + + // NOTE: we don't commit, but BeginBlock for block 1 + // starts from this deliverState + return +} +func (app *BaseApp) setDeliverState(header abci.Header) { + ms := app.cms.CacheMultiStore() + app.deliverState = &state{ + ms: ms, + ctx: sdk.NewContext(ms, header, false, nil, app.Logger), + } +} + +``` +
+当这些信息被正确的处理后,比如是一个帐户相关的信息,那么就可以使用它来进行交易的处理了。 +
+2、BeginBlock +
+在上面提到过Tendermint的三种消息,其中的交易处理消息DeliverTx,它就是在区块开始被调用前,在这个接口中处理验证人签名的信息。如果大家写过数据库的底层操作,这个东西应该和它非常类似,不外乎是Begin准备,End结束,清扫资源。不过使用它的时候儿也需要注意,它和其它的相类似的操作一样,在这两个函数的处理过程中,不应该包含过多的和过于复杂的操作,导致整个消息的阻塞。 +
+如果在这二者中出现了不合理的循环等,就有可能导致应用程序APP的假死。 +
+ +``` golang +// application updates every end block +func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + tags := slashing.BeginBlocker(ctx, req, app.slashingKeeper) + + return abci.ResponseBeginBlock{ + Tags: tags.ToKVPairs(), + } +} +// slashing begin block functionality +func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, sk Keeper) (tags sdk.Tags) { + // Tag the height + heightBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(heightBytes, uint64(req.Header.Height)) + tags = sdk.NewTags("height", heightBytes) + + // Deal with any equivocation evidence + for _, evidence := range req.ByzantineValidators { + pk, err := tmtypes.PB2TM.PubKey(evidence.Validator.PubKey) + if err != nil { + panic(err) + } + switch string(evidence.Type) { + case tmtypes.ABCIEvidenceTypeDuplicateVote: + //处理验证器在同一高度签名两个块 + sk.handleDoubleSign(ctx, evidence.Height, evidence.Time, pk) + default: + ctx.Logger().With("module", "x/slashing").Error(fmt.Sprintf("Ignored unknown evidence type: %s", string(evidence.Type))) + } + } + + // Iterate over all the validators which *should* have signed this block + for _, validator := range req.Validators { + present := validator.SignedLastBlock + pubkey, err := tmtypes.PB2TM.PubKey(validator.Validator.PubKey) + if err != nil { + panic(err) + } + sk.handleValidatorSignature(ctx, pubkey, present) + } + + return +} +``` +
+3、EndBlock +
+响应上一个函数接口,在DeliverTx消息处理完成所有的交易后调用,主要用来对验证人集合的结果进行维护。 +
+ +``` golang +// Implements ABCI +func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { + if app.endBlocker != nil { + res = app.endBlocker(app.deliverState.ctx, req) + } else { + res.ValidatorUpdates = app.valUpdates + } + return +} +``` +
+4、Commit +
+当处理完成交易后,应该把完成的交易从内存持久化到硬盘上,并以上为根据创建返回被下一个Tendermint区块需要的默克尔树的Root哈希值。这个哈希值 的作用在区块链中基本是一样的,用来验证合法性。 +
+ +``` golang +// Implements ABCI +func (app *BaseApp) Commit() (res abci.ResponseCommit) { + header := app.deliverState.ctx.BlockHeader() + /* + // Write the latest Header to the store + headerBytes, err := proto.Marshal(&header) + if err != nil { + panic(err) + } + app.db.SetSync(dbHeaderKey, headerBytes) + */ + + // Write the Deliver state and commit the MultiStore + app.deliverState.ms.Write() + commitID := app.cms.Commit() + app.Logger.Debug("Commit synced", + "commit", commitID, + ) + + // Reset the Check state to the latest committed + // NOTE: safe because Tendermint holds a lock on the mempool for Commit. + // Use the header from this latest block. + app.setCheckState(header) + + // Empty the Deliver state + app.deliverState = nil + + return abci.ResponseCommit{ + Data: commitID.Hash, + } +} +``` +
+5、Query +
+这个就不多说了吧,你总得给别人一个看一看的机会。 +
+ +``` golang +// Implements ABCI. +// Delegates to CommitMultiStore if it implements Queryable +func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { + path := strings.Split(req.Path, "/") + // first element is empty string + if len(path) > 0 && path[0] == "" { + path = path[1:] + } + // "/app" prefix for special application queries + if len(path) >= 2 && path[0] == "app" { + var result sdk.Result + switch path[1] { + case "simulate": + txBytes := req.Data + tx, err := app.txDecoder(txBytes) + if err != nil { + result = err.Result() + } else { + result = app.Simulate(tx) + } + default: + result = sdk.ErrUnknownRequest(fmt.Sprintf("Unknown query: %s", path)).Result() + } + value := app.cdc.MustMarshalBinary(result) + return abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Value: value, + } + } + // "/store" prefix for store queries + if len(path) >= 1 && path[0] == "store" { + queryable, ok := app.cms.(sdk.Queryable) + if !ok { + msg := "multistore doesn't support queries" + return sdk.ErrUnknownRequest(msg).QueryResult() + } + req.Path = "/" + strings.Join(path[1:], "/") + return queryable.Query(req) + } + // "/p2p" prefix for p2p queries + if len(path) >= 4 && path[0] == "p2p" { + if path[1] == "filter" { + if path[2] == "addr" { + return app.FilterPeerByAddrPort(path[3]) + } + if path[2] == "pubkey" { + return app.FilterPeerByPubKey(path[3]) + } + } + } + msg := "unknown query path" + return sdk.ErrUnknownRequest(msg).QueryResult() +} +``` +
+6、CheckTx +
+所有的拥有交易池的区块链,基本上在进池子前后都要搞一些事情,包括对各种合法性的检查,目的只有一个,防止千辛万苦才生产来的区块打包一些没用的交易。在Cosmos中也会有这种手段,在前面提到过AnteHandler,通过其对发送者授权,确定在交易前有足够的手续费,不过它和以太坊有些类似,如果交易失败,这笔费用仍然没有了,收不回去。 +
+ +``` golang +// Implements ABCI +func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { + // Decode the Tx. + var result sdk.Result + var tx, err = app.txDecoder(txBytes) + if err != nil { + result = err.Result() + } else { + result = app.runTx(runTxModeCheck, txBytes, tx) + } + + return abci.ResponseCheckTx{ + Code: uint32(result.Code), + Data: result.Data, + Log: result.Log, + GasWanted: result.GasWanted, + GasUsed: result.GasUsed, + Fee: cmn.KI64Pair{ + []byte(result.FeeDenom), + result.FeeAmount, + }, + Tags: result.Tags, + } +} +``` +
+ +### 3、IBC通信源码 +
+在前面的代码中初始化时需要对路由进行注册,在这里同样会有路由的实际注册过程,先看一看提供的命令处理方式: +
+ +``` golang +// IBC transfer command +func IBCTransferCmd(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + + // get the from address + from, err := ctx.GetFromAddress() + if err != nil { + return err + } + + // build the message + msg, err := buildMsg(from) + if err != nil { + return err + } + + // get password + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } + + cmd.Flags().String(flagTo, "", "Address to send coins") + cmd.Flags().String(flagAmount, "", "Amount of coins to send") + cmd.Flags().String(flagChain, "", "Destination chain to send coins") + return cmd +} +``` +
+处理传输命令,进入中继环节处理: +
+ +``` golang +// flags--代表从一个空间转向另外一个窠 +const ( + FlagFromChainID = "from-chain-id" + FlagFromChainNode = "from-chain-node" + FlagToChainID = "to-chain-id" + FlagToChainNode = "to-chain-node" +) + +type relayCommander struct { + cdc *wire.Codec + address sdk.Address + decoder auth.AccountDecoder + mainStore string + ibcStore string + accStore string + + logger log.Logger +} + +// IBC relay command +func IBCRelayCmd(cdc *wire.Codec) *cobra.Command { + cmdr := relayCommander{ + cdc: cdc, + decoder: authcmd.GetAccountDecoder(cdc), + ibcStore: "ibc", + mainStore: "main", + accStore: "acc", + + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)), + } + + cmd := &cobra.Command{ + Use: "relay", + Run: cmdr.runIBCRelay, + } + + cmd.Flags().String(FlagFromChainID, "", "Chain ID for ibc node to check outgoing packets") + cmd.Flags().String(FlagFromChainNode, "tcp://localhost:46657", ": to tendermint rpc interface for this chain") + cmd.Flags().String(FlagToChainID, "", "Chain ID for ibc node to broadcast incoming packets") + cmd.Flags().String(FlagToChainNode, "tcp://localhost:36657", ": to tendermint rpc interface for this chain") + + cmd.MarkFlagRequired(FlagFromChainID) + cmd.MarkFlagRequired(FlagFromChainNode) + cmd.MarkFlagRequired(FlagToChainID) + cmd.MarkFlagRequired(FlagToChainNode) + + viper.BindPFlag(FlagFromChainID, cmd.Flags().Lookup(FlagFromChainID)) + viper.BindPFlag(FlagFromChainNode, cmd.Flags().Lookup(FlagFromChainNode)) + viper.BindPFlag(FlagToChainID, cmd.Flags().Lookup(FlagToChainID)) + viper.BindPFlag(FlagToChainNode, cmd.Flags().Lookup(FlagToChainNode)) + + return cmd +} + +//启动遍历监听 +func (c relayCommander) runIBCRelay(cmd *cobra.Command, args []string) { + fromChainID := viper.GetString(FlagFromChainID) + fromChainNode := viper.GetString(FlagFromChainNode) + toChainID := viper.GetString(FlagToChainID) + toChainNode := viper.GetString(FlagToChainNode) + address, err := context.NewCoreContextFromViper().GetFromAddress() + if err != nil { + panic(err) + } + c.address = address + + c.loop(fromChainID, fromChainNode, toChainID, toChainNode) +} + +func (c relayCommander) loop(fromChainID, fromChainNode, toChainID, + toChainNode string) { + + ctx := context.NewCoreContextFromViper() + // get password + passphrase, err := ctx.GetPassphraseFromStdin(ctx.FromAddressName) + if err != nil { + panic(err) + } + + ingressKey := ibc.IngressSequenceKey(fromChainID) + +OUTER: + for { + time.Sleep(5 * time.Second) + + processedbz, err := query(toChainNode, ingressKey, c.ibcStore) + if err != nil { + panic(err) + } + + var processed int64 + if processedbz == nil { + processed = 0 + } else if err = c.cdc.UnmarshalBinary(processedbz, &processed); err != nil { + panic(err) + } + + lengthKey := ibc.EgressLengthKey(toChainID) + egressLengthbz, err := query(fromChainNode, lengthKey, c.ibcStore) + if err != nil { + c.logger.Error("Error querying outgoing packet list length", "err", err) + continue OUTER //TODO replace with continue (I think it should just to the correct place where OUTER is now) + } + var egressLength int64 + if egressLengthbz == nil { + egressLength = 0 + } else if err = c.cdc.UnmarshalBinary(egressLengthbz, &egressLength); err != nil { + panic(err) + } + if egressLength > processed { + c.logger.Info("Detected IBC packet", "number", egressLength-1) + } + + seq := c.getSequence(toChainNode) + + for i := processed; i < egressLength; i++ { + egressbz, err := query(fromChainNode, ibc.EgressKey(toChainID, i), c.ibcStore) + if err != nil { + c.logger.Error("Error querying egress packet", "err", err) + continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER) + } + + err = c.broadcastTx(seq, toChainNode, c.refine(egressbz, i, passphrase)) + seq++ + if err != nil { + c.logger.Error("Error broadcasting ingress packet", "err", err) + continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER) + } + + c.logger.Info("Relayed IBC packet", "number", i) + } + } +} + +func (c relayCommander) broadcastTx(seq int64, node string, tx []byte) error { + _, err := context.NewCoreContextFromViper().WithNodeURI(node).WithSequence(seq + 1).BroadcastTx(tx) + return err +} + +//处理接收的消息 +func (c relayCommander) refine(bz []byte, sequence int64, passphrase string) []byte { + var packet ibc.IBCPacket + if err := c.cdc.UnmarshalBinary(bz, &packet); err != nil { + panic(err) + } + + msg := ibc.IBCReceiveMsg{ + IBCPacket: packet, + Relayer: c.address, + Sequence: sequence, + } + + ctx := context.NewCoreContextFromViper().WithSequence(sequence) + res, err := ctx.SignAndBuild(ctx.FromAddressName, passphrase, msg, c.cdc) + if err != nil { + panic(err) + } + return res +} +``` +
+通过一个中继节点来监听两条不同的链,进行消息的路由注册来达到自动跨链交易,Cosmos提供的这个方式还是比较不错的。至少,不用自己再犯愁怎么做。但是这个有一个前提,需要注册一下: +
+ +``` golang +// RegisterRoutes - Central function to define routes that get registered by the main application +func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + r.HandleFunc("/ibc/{destchain}/{address}/send", TransferRequestHandlerFn(cdc, kb, ctx)).Methods("POST") +} +``` +
+ +## 三、总结 +
+通过上面的简单的代码分析,可以看出,ABCI和IBC两个模块,是运行整个Cosmos的一个基础。Cosmos-SDK把这几个模块有机的抽象到一起,并提供了基础的交易、通信等功能。新的区块链可以从它上面调用或者继承Example中的例程,只关心区块链功能的开发,短时间内就可以方便的开发出一条公链来。 +
diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\247\224\346\211\230\344\272\272delegators.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\247\224\346\211\230\344\272\272delegators.md" new file mode 100644 index 0000000..41e4cd3 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\247\224\346\211\230\344\272\272delegators.md" @@ -0,0 +1,218 @@ + +# cosmos源码分析之五委托人delegators +## 一、委托人 +在整个的cosmos中,委托人是一个非常重要的角色。通过委托机制可以达到全民参与的形式。这在EOS中也有体现。委托人,其实就是本身无力或者不想参与验证过程的人,他们把自己的权益(代币份额)交由某个人来代替执行自己的权益。 +
+在整个的网络中,验证人的数量是有限的,正如美国大选,总统和议员总是少数,但是他们是由美国公民选举出来的,可以把美国公民理解成委托人。当他们把票投给某个人时,某个人当选,然后当选的人就会有倾向性的做一些工作来回馈投票人。 +
+在cosmos中,道理是相通的。 +
+ +## 二、委托的流程 + +1、选择验证人 +
+在委托前,还是要找一个利益最大化的验证人来做为自己的委托者。参照的信息包括: +
+验证人的名称、介绍、佣金变化率、最大佣金、最小抵押数量以及初始化佣金比例等。 +
+2、委托人的说明 +
+1)委托人应该对验证人进行仔细调查,因为一理验证人有问题,相应的委托人会跟随的被惩罚。 +
+2)委托人在委托后也要积极监控验证人,保证其合法进行工作,一旦有任何不满意的地方,可以解绑并转身另外一个验证人。 +
+3)委托人可以通过投票权来制衡他们的验证人。 +
+3、收益 +
+在前面提到过为了抵抗通货膨胀,会定期的增发Atom分配给抵押者。这就是一种收益,那么收益有哪些方面呢? +
+1)区块增发的Atom奖励。 +
+2)区块奖励(photon)。比如对硬分叉的投票表决时会有奖励。 +
+3)交易费用,这个不用细说,几乎所有的链都有这块费用。 +
+4、佣金 +
+就如稳健型的投资一样,每个验证人的股权池会根据抵押比例获取收益(利息)。不过,在将这笔收益按比例返给委托人时,验证人有权抽取一笔佣金(手续费),换句话说,委托人想获取收益,就必须向自己委托的验证人提供一笔管理费。 +
+这里需要说明的是,佣金是委托人给予验证人的。而收益是委托人拿到自己帐户中的。 +
+5、风险 +
+把钱给别人用,这个风险是肯定有的。虚拟网络和真实社会没有什么革命性的区别。那么风险体现在哪儿呢?验证人在行使权力的时候儿,Atom是被锁仓的,首先没办法使用,再者,如果验证人犯错误,Atom是要被罚减的。这里面,就包含委托人的代币。什么原因会导致被处罚呢? +
+1)重复签名:如果钓鱼者反馈一个验证人在多条链上的相同高度上多次签名,那么就会被惩罚。 +
+2)不完成工作:也就是说不投票,占着那个不那个。也会被惩罚。 +
+3)玩失踪:这和2有些类似。 +
+ +## 三、源码 +
+委托人和验证人以及stake相关的部分中会有很强的关系。 +
+stake.go +
+ +``` go +// delegation bond for a delegated proof of stake system +type Delegation interface { + GetDelegator() Address // delegator address for the bond + GetValidator() Address // validator owner address for the bond + GetBondShares() Rat // amount of validator's shares +} + +// properties for the set of all delegations for a particular +type DelegationSet interface { + + // iterate through all delegations from one delegator by validator-address, + // execute func for each validator + IterateDelegators(Context, delegator Address, + fn func(index int64, delegation Delegation) (stop bool)) +} +``` +
+委托人的管理封装数据结构。 +
+delegation.go +
+ +``` go +// Delegation represents the bond with tokens held by an account. It is +// owned by one delegator, and is associated with the voting power of one +// pubKey. +// TODO better way of managing space +type Delegation struct { + DelegatorAddr sdk.Address `json:"delegator_addr"` + ValidatorAddr sdk.Address `json:"validator_addr"` + Shares sdk.Rat `json:"shares"` + Height int64 `json:"height"` // Last height bond updated +} + +func (b Delegation) equal(b2 Delegation) bool { + return bytes.Equal(b.DelegatorAddr, b2.DelegatorAddr) && + bytes.Equal(b.ValidatorAddr, b2.ValidatorAddr) && + b.Height == b2.Height && + b.Shares.Equal(b2.Shares) +} + +// ensure fulfills the sdk validator types +var _ sdk.Delegation = Delegation{} + +// nolint - for sdk.Delegation +func (b Delegation) GetDelegator() sdk.Address { return b.DelegatorAddr } +func (b Delegation) GetValidator() sdk.Address { return b.ValidatorAddr } +func (b Delegation) GetBondShares() sdk.Rat { return b.Shares } + +//Human Friendly pretty printer +func (b Delegation) HumanReadableString() (string, error) { + bechAcc, err := sdk.Bech32ifyAcc(b.DelegatorAddr) + if err != nil { + return "", err + } + bechVal, err := sdk.Bech32ifyAcc(b.ValidatorAddr) + if err != nil { + return "", err + } + resp := "Delegation \n" + resp += fmt.Sprintf("Delegator: %s\n", bechAcc) + resp += fmt.Sprintf("Validator: %s\n", bechVal) + resp += fmt.Sprintf("Shares: %s", b.Shares.String()) + resp += fmt.Sprintf("Height: %d", b.Height) + + return resp, nil + +} +``` +
+这里需要在处理的handle中控制绑定的委托者和验证人 +
+handler.go +
+ +``` go +// common functionality between handlers +func delegate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address, + bondAmt sdk.Coin, validator Validator) (sdk.Tags, sdk.Error) { + + // Get or create the delegator bond + bond, found := k.GetDelegation(ctx, delegatorAddr, validator.Owner) + if !found { + bond = Delegation{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validator.Owner, + Shares: sdk.ZeroRat(), + } + } + + // Account new shares, save + pool := k.GetPool(ctx) + _, _, err := k.coinKeeper.SubtractCoins(ctx, bond.DelegatorAddr, sdk.Coins{bondAmt}) + if err != nil { + return nil, err + } + validator, pool, newShares := validator.addTokensFromDel(pool, bondAmt.Amount) + bond.Shares = bond.Shares.Add(newShares) + + // Update bond height + bond.Height = ctx.BlockHeight() + + k.setPool(ctx, pool) + k.setDelegation(ctx, bond) + k.updateValidator(ctx, validator) + tags := sdk.NewTags("action", []byte("delegate"), "delegator", delegatorAddr.Bytes(), "validator", validator.Owner.Bytes()) + return tags, nil +} +``` +
+ +将委托者和验证人绑定在一起是通过Msg的bond和unbond来实现的。 +
+ +msg.go: +
+ +``` go +// MsgDelegate - struct for bonding transactions +type MsgDelegate struct { + DelegatorAddr sdk.Address `json:"delegator_addr"` + ValidatorAddr sdk.Address `json:"validator_addr"` + Bond sdk.Coin `json:"bond"` +} + +func NewMsgDelegate(delegatorAddr, validatorAddr sdk.Address, bond sdk.Coin) MsgDelegate { + return MsgDelegate{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + Bond: bond, + } +} + +// MsgUnbond - struct for unbonding transactions +type MsgUnbond struct { + DelegatorAddr sdk.Address `json:"delegator_addr"` + ValidatorAddr sdk.Address `json:"validator_addr"` + Shares string `json:"shares"` +} + +func NewMsgUnbond(delegatorAddr, validatorAddr sdk.Address, shares string) MsgUnbond { + return MsgUnbond{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + Shares: shares, + } +} +``` +
+ +# 四、总结 + +通过上面的分析和说明,可以看出,委托人其实就是普通的网络节点,验证人的资格也不是你想有就有的。POS的机制决定了跟和资本主义社会一样,没有足够的金钱,没法参加大选。 +
+这也是POS饱受诟病的地方,在COSMOS中也有一些防止的方法,但目前看来还不能从根本上解决问题。所以委托人在委托时,还是不要任性。 +
+ diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253Tendermint\345\220\257\345\212\250\345\210\206\346\236\220.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253Tendermint\345\220\257\345\212\250\345\210\206\346\236\220.md" new file mode 100644 index 0000000..1e766f9 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253Tendermint\345\220\257\345\212\250\345\210\206\346\236\220.md" @@ -0,0 +1,588 @@ +# cosmos源码分析之八Tendermint启动分析 + +## 一、启动流程介绍 +Tendermint的启动流程比较简单,可能跟它没有相关的业务场景有关系,大体分为三步:第一步配置命令选项;第二步启动并运行节点;第三步,启动相关命令。代码上看上去也要比以太坊等其它链的启动代码更简洁。 +
+这里需要说明的是,它用的命令行工具是cobra这个第三方的工具,不是以太坊等用的cli。 + +## 二、入口 +先看一下入口的函数: +
+ +``` golang +func main() { + rootCmd := cmd.RootCmd + rootCmd.AddCommand( + cmd.GenValidatorCmd, + cmd.InitFilesCmd, + cmd.ProbeUpnpCmd, + cmd.LiteCmd, + cmd.ReplayCmd, + cmd.ReplayConsoleCmd, + cmd.ResetAllCmd, + cmd.ResetPrivValidatorCmd, + cmd.ShowValidatorCmd, + cmd.TestnetFilesCmd, + cmd.ShowNodeIDCmd, + cmd.GenNodeKeyCmd, + cmd.VersionCmd) + + // NOTE: + // Users wishing to: + // * Use an external signer for their validators + // * Supply an in-proc abci app + // * Supply a genesis doc file from another source + // * Provide their own DB implementation + // can copy this file and use something other than the + // DefaultNewNode function + nodeFunc := nm.DefaultNewNode + + // Create & start node + rootCmd.AddCommand(cmd.NewRunNodeCmd(nodeFunc)) + + cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv(filepath.Join("$HOME", cfg.DefaultTendermintDir))) + if err := cmd.Execute(); err != nil { + panic(err) + } +} +``` +
+是不是非常简单。它的根命令操作如下: +
+ +``` golang +// RootCmd is the root command for Tendermint core. +var RootCmd = &cobra.Command{ + Use: "tendermint", + Short: "Tendermint Core (BFT Consensus) in Go", + PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { + if cmd.Name() == VersionCmd.Name() { + return nil + } + config, err = ParseConfig() + ...... + logger = logger.With("module", "main") + return nil + }, +} +``` +
+在上面的代码中,会调用cmd.NewRunNodeCmd(nodeFunc),而在它上面一行代码nodeFunc := nm.DefaultNewNode已经创建了运行参数,这个函数的目的其实很简单,创建Node并启动,同时开始监听,看一下它的代码(cmd/tendermint/commands/run_node.go): +
+ +``` golang +// NewRunNodeCmd returns the command that allows the CLI to start a node. +// It can be used with a custom PrivValidator and in-process ABCI application. +func NewRunNodeCmd(nodeProvider nm.NodeProvider) *cobra.Command { + cmd := &cobra.Command{ + Use: "node", + Short: "Run the tendermint node", + RunE: func(cmd *cobra.Command, args []string) error { + // Create & start node + n, err := nodeProvider(config, logger) + if err != nil { + return fmt.Errorf("Failed to create node: %v", err) + } + + if err := n.Start(); err != nil { + return fmt.Errorf("Failed to start node: %v", err) + } + logger.Info("Started node", "nodeInfo", n.Switch().NodeInfo()) + + // Trap signal, run forever. + n.RunForever() + + return nil + }, + } + + AddNodeFlags(cmd) + return cmd +} +``` +
+其时它就是利用了Node节点的默认的创建函数传送到Cmd中进行节点的创建,创建完成后使用Start函数将其启动。在最后使用节点的信号监听函数来处理相关的消息。 + +## 三、创建NODE +在上面的分析知道,命令运行开始创建Node,先看一下Node的数据结构: +
+ +``` golang +type Node struct { + cmn.BaseService + + // config + config *cfg.Config + genesisDoc *types.GenesisDoc // initial validator set + privValidator types.PrivValidator // local node's validator key + + // network + sw *p2p.Switch // p2p connections + addrBook pex.AddrBook // known peers + + // services + eventBus *types.EventBus // pub/sub for services + stateDB dbm.DB + blockStore *bc.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing + mempoolReactor *mempl.MempoolReactor // for gossipping transactions + consensusState *cs.ConsensusState // latest consensus state + consensusReactor *cs.ConsensusReactor // for participating in the consensus + evidencePool *evidence.EvidencePool // tracking evidence + proxyApp proxy.AppConns // connection to the application + rpcListeners []net.Listener // rpc servers + txIndexer txindex.TxIndexer + indexerService *txindex.IndexerService +} +``` +
+看一下上面的调用的创建Node的函数: +
+ +``` golang +// DefaultNewNode returns a Tendermint node with default settings for the +// PrivValidator, ClientCreator, GenesisDoc, and DBProvider. +// It implements NodeProvider. +func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { + return NewNode(config, + pvm.LoadOrGenFilePV(config.PrivValidatorFile()), + proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + DefaultGenesisDocProviderFunc(config), + DefaultDBProvider, + logger, + ) +} +``` +
+NewNode这个函数太长了,这里节选一下: +
+ +``` golang +// NewNode returns a new, ready to go, Tendermint Node. +func NewNode(config *cfg.Config, + privValidator types.PrivValidator, + clientCreator proxy.ClientCreator, + genesisDocProvider GenesisDocProvider, + dbProvider DBProvider, + logger log.Logger) (*Node, error) { + + // Get BlockStore + blockStoreDB, err := dbProvider(&DBContext{"blockstore", config}) + if err != nil { + return nil, err + } + blockStore := bc.NewBlockStore(blockStoreDB) + + // Get State + stateDB, err := dbProvider(&DBContext{"state", config}) + if err != nil { + return nil, err + } +...... + + // Make MempoolReactor + mempoolLogger := logger.With("module", "mempool") + mempool := mempl.NewMempool(config.Mempool, proxyApp.Mempool(), state.LastBlockHeight) + mempool.InitWAL() // no need to have the mempool wal during tests + mempool.SetLogger(mempoolLogger) + mempoolReactor := mempl.NewMempoolReactor(config.Mempool, mempool) + mempoolReactor.SetLogger(mempoolLogger) + + if config.Consensus.WaitForTxs() { + mempool.EnableTxsAvailable() + } + + // Make Evidence Reactor + evidenceDB, err := dbProvider(&DBContext{"evidence", config}) + if err != nil { + return nil, err + } + evidenceLogger := logger.With("module", "evidence") + evidenceStore := evidence.NewEvidenceStore(evidenceDB) + evidencePool := evidence.NewEvidencePool(stateDB, evidenceStore) + evidencePool.SetLogger(evidenceLogger) + evidenceReactor := evidence.NewEvidenceReactor(evidencePool) + evidenceReactor.SetLogger(evidenceLogger) + + blockExecLogger := logger.With("module", "state") + // make block executor for consensus and blockchain reactors to execute blocks + blockExec := sm.NewBlockExecutor(stateDB, blockExecLogger, proxyApp.Consensus(), mempool, evidencePool) + + // Make BlockchainReactor + bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor.SetLogger(logger.With("module", "blockchain")) + + // Make ConsensusReactor + consensusState := cs.NewConsensusState(config.Consensus, state.Copy(), + blockExec, blockStore, mempool, evidencePool) + consensusState.SetLogger(consensusLogger) + if privValidator != nil { + consensusState.SetPrivValidator(privValidator) + } + consensusReactor := cs.NewConsensusReactor(consensusState, fastSync) + consensusReactor.SetLogger(consensusLogger) + + p2pLogger := logger.With("module", "p2p") + + sw := p2p.NewSwitch(config.P2P) +...... + addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) +....... + + eventBus := types.NewEventBus() + eventBus.SetLogger(logger.With("module", "events")) + + // services which will be publishing and/or subscribing for messages (events) + // consensusReactor will set it on consensusState and blockExecutor + consensusReactor.SetEventBus(eventBus) + +...... + + node := &Node{ + config: config, + genesisDoc: genDoc, + privValidator: privValidator, + + sw: sw, + addrBook: addrBook, + + stateDB: stateDB, + blockStore: blockStore, + bcReactor: bcReactor, + mempoolReactor: mempoolReactor, + consensusState: consensusState, + consensusReactor: consensusReactor, + evidencePool: evidencePool, + proxyApp: proxyApp, + txIndexer: txIndexer, + indexerService: indexerService, + eventBus: eventBus, + } + node.BaseService = *cmn.NewBaseService(logger, "Node", node) + return node, nil +} +``` +
+启动的Start函数会调用Node.go中的OnStart函数来实现Node的启动: +
+ +``` golang +// OnStart starts the Node. It implements cmn.Service. +func (n *Node) OnStart() error { + err := n.eventBus.Start() + if err != nil { + return err + } + + // Create & add listener + protocol, address := cmn.ProtocolAndAddress(n.config.P2P.ListenAddress) + l := p2p.NewDefaultListener(protocol, address, n.config.P2P.SkipUPNP, n.Logger.With("module", "p2p")) + n.sw.AddListener(l) + + // Generate node PrivKey + // TODO: pass in like privValidator + nodeKey, err := p2p.LoadOrGenNodeKey(n.config.NodeKeyFile()) + if err != nil { + return err + } + n.Logger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", n.config.NodeKeyFile()) + + nodeInfo := n.makeNodeInfo(nodeKey.ID()) + n.sw.SetNodeInfo(nodeInfo) + n.sw.SetNodeKey(nodeKey) + + // Add ourselves to addrbook to prevent dialing ourselves + n.addrBook.AddOurAddress(nodeInfo.NetAddress()) + + // Start the RPC server before the P2P server + // so we can eg. receive txs for the first block + if n.config.RPC.ListenAddress != "" { + listeners, err := n.startRPC() + if err != nil { + return err + } + n.rpcListeners = listeners + } + + // Start the switch (the P2P server). + err = n.sw.Start() + if err != nil { + return err + } + + // Always connect to persistent peers + if n.config.P2P.PersistentPeers != "" { + err = n.sw.DialPeersAsync(n.addrBook, cmn.SplitAndTrim(n.config.P2P.PersistentPeers, ",", " "), true) + if err != nil { + return err + } + } + + // start tx indexer + return n.indexerService.Start() +} +``` +
+需要说明的是,这个Start函数位于"github.com/tendermint/tmlibs/common"中的server.go中。有一个好的IDE,同时网络好能自动下载相关的包真是太羡慕了。 +
+同样,启动监听信号的函数也是在上面的库中,即: +
+ +``` golang +// RunForever waits for an interrupt signal and stops the node. +func (n *Node) RunForever() { + // Sleep forever and then... + //下面这个信号处理 + cmn.TrapSignal(func() { + n.Stop() + }) +} +``` +
+节点的停止就要被它监控: +
+ +``` golang +func (n *Node) OnStop() { + n.BaseService.OnStop() + + n.Logger.Info("Stopping Node") + // TODO: gracefully disconnect from peers. + n.sw.Stop() + + for _, l := range n.rpcListeners { + n.Logger.Info("Closing rpc listener", "listener", l) + if err := l.Close(); err != nil { + n.Logger.Error("Error closing listener", "listener", l, "err", err) + } + } + + n.eventBus.Stop() + n.indexerService.Stop() + + if pvsc, ok := n.privValidator.(*pvm.SocketPV); ok { + if err := pvsc.Stop(); err != nil { + n.Logger.Error("Error stopping priv validator socket client", "err", err) + } + } +} +``` +
+节点启动停止可以进行控制后,就可以进行在节点上的各种动作了,如共识、通信等。 +
+ +## 四、网络通信 +网络通信的启停就在上面的OnStart函数中,分成了两部分,服务监听和拨号,在服务监听中首先注册P2P的协议,这个和以太坊类似,然后创建监听器并添加到相关的节点中。看下面的代码: +
+ +``` golang +// skipUPNP: If true, does not try getUPNPExternalAddress() +func NewDefaultListener(protocol string, lAddr string, skipUPNP bool, logger log.Logger) Listener { + // Local listen IP & port + lAddrIP, lAddrPort := splitHostPort(lAddr) + + // Create listener + var listener net.Listener + var err error + for i := 0; i < tryListenSeconds; i++ { + listener, err = net.Listen(protocol, lAddr) + if err == nil { + break + } else if i < tryListenSeconds-1 { + time.Sleep(time.Second * 1) + } + } + if err != nil { + panic(err) + } + // Actual listener local IP & port + listenerIP, listenerPort := splitHostPort(listener.Addr().String()) + logger.Info("Local listener", "ip", listenerIP, "port", listenerPort) + + // Determine internal address... + var intAddr *NetAddress + intAddr, err = NewNetAddressStringWithOptionalID(lAddr) + if err != nil { + panic(err) + } + + // Determine external address... + var extAddr *NetAddress + if !skipUPNP { + // If the lAddrIP is INADDR_ANY, try UPnP + if lAddrIP == "" || lAddrIP == "0.0.0.0" { + extAddr = getUPNPExternalAddress(lAddrPort, listenerPort, logger) + } + } + // Otherwise just use the local address... + if extAddr == nil { + extAddr = getNaiveExternalAddress(listenerPort, false, logger) + } + if extAddr == nil { + panic("Could not determine external address!") + } + + dl := &DefaultListener{ + listener: listener, + intAddr: intAddr, + extAddr: extAddr, + connections: make(chan net.Conn, numBufferedConnections), + } + dl.BaseService = *cmn.NewBaseService(logger, "DefaultListener", dl) + err = dl.Start() // Started upon construction + if err != nil { + logger.Error("Error starting base service", "err", err) + } + return dl +} +func (l *DefaultListener) OnStart() error { + if err := l.BaseService.OnStart(); err != nil { + return err + } + go l.listenRoutine() + return nil +} + +func (l *DefaultListener) OnStop() { + l.BaseService.OnStop() + l.listener.Close() // nolint: errcheck +} +``` +
+同上面一样,会启动这个监听,前面省略了RPC的启动。这个没有啥特殊之处。 +
+有了服务,然后再启动连接: +
+ +``` golang +func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent bool) error { + netAddrs, errs := NewNetAddressStrings(peers) + // only log errors, dial correct addresses + for _, err := range errs { + sw.Logger.Error("Error in peer's address", "err", err) + } + + ourAddr := sw.nodeInfo.NetAddress() + + // TODO: this code feels like it's in the wrong place. + // The integration tests depend on the addrBook being saved + // right away but maybe we can change that. Recall that + // the addrBook is only written to disk every 2min + if addrBook != nil { + // add peers to `addrBook` + for _, netAddr := range netAddrs { + // do not add our address or ID + if !netAddr.Same(ourAddr) { + if err := addrBook.AddAddress(netAddr, ourAddr); err != nil { + sw.Logger.Error("Can't add peer's address to addrbook", "err", err) + } + } + } + // Persist some peers to disk right away. + // NOTE: integration tests depend on this + addrBook.Save() + } + + // permute the list, dial them in random order. + perm := sw.rng.Perm(len(netAddrs)) + for i := 0; i < len(perm); i++ { + go func(i int) { + j := perm[i] + + addr := netAddrs[j] + // do not dial ourselves + if addr.Same(ourAddr) { + return + } + +...... + }(i) + } + return nil +} + +``` +
+ +## 五、共识 +网络启动后,共识启动的时机也就成熟了,毕竟共识是需要在多节点间进行通信的,P2P不起来,共识也没法完成工作,那么共识在哪里开始启动的呢? +
+在创建Node的函数NewNode中,会创建共识的反应器NewConsensusReactor,并将其添加eventBus中。同样也会在NewNode中进行NewConsensusState,这样在启动节点时会调用总线事件启动: +
+ +``` golang +func (conR *ConsensusReactor) OnStart() error { + conR.Logger.Info("ConsensusReactor ", "fastSync", conR.FastSync()) + if err := conR.BaseReactor.OnStart(); err != nil { + return err + } + + conR.subscribeToBroadcastEvents() + + if !conR.FastSync() { + err := conR.conS.Start() + if err != nil { + return err + } + } + + return nil +} + +// OnStart implements cmn.Service. +// It loads the latest state via the WAL, and starts the timeout and receive routines. +func (cs *ConsensusState) OnStart() error { + if err := cs.evsw.Start(); err != nil { + return err + } + + // we may set the WAL in testing before calling Start, + // so only OpenWAL if its still the nilWAL + if _, ok := cs.wal.(nilWAL); ok { + walFile := cs.config.WalFile() + wal, err := cs.OpenWAL(walFile) + if err != nil { + cs.Logger.Error("Error loading ConsensusState wal", "err", err.Error()) + return err + } + cs.wal = wal + } + + // we need the timeoutRoutine for replay so + // we don't block on the tick chan. + // NOTE: we will get a build up of garbage go routines + // firing on the tockChan until the receiveRoutine is started + // to deal with them (by that point, at most one will be valid) + if err := cs.timeoutTicker.Start(); err != nil { + return err + } + + // we may have lost some votes if the process crashed + // reload from consensus log to catchup + if cs.doWALCatchup { + if err := cs.catchupReplay(cs.Height); err != nil { + cs.Logger.Error("Error on catchup replay. Proceeding to start ConsensusState anyway", "err", err.Error()) + // NOTE: if we ever do return an error here, + // make sure to stop the timeoutTicker + } + } + + // now start the receiveRoutine + go cs.receiveRoutine(0) + + // schedule the first round! + // use GetRoundState so we don't race the receiveRoutine for access + cs.scheduleRound0(cs.GetRoundState()) + + return nil +} +``` +
+再下来,就是共识的过程了,这个在前面的分析里简单的说明一下,这个共识的过程其实没有什么太复杂之处,看代码结合着Tendermint的文档即可。 +
+ +## 六、总结 +通过上面的代码的简单分析,基本了解了整个Tendermint的启动流程,其实诸如IBC通信等这里都没有深入展开介绍,如果有兴趣可以去GITHUB上下来源码认真的比对着相关文档看看。 +
+正如Tendermint的分而治之的思想一样,一个模块一个模块的分析Tendermint,其实也没有什么特别的难点。 +
diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\351\252\214\350\257\201\344\272\272Validators.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\351\252\214\350\257\201\344\272\272Validators.md" new file mode 100644 index 0000000..c21e8b7 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\351\252\214\350\257\201\344\272\272Validators.md" @@ -0,0 +1,345 @@ +# cosmos源码分析之六验证人 +## 一、简介 +在整个cosmos中,验证人的角色是非常重要的,它们负责投票决定向区块链提交新的区块。或者可以理解成,没有验证人,就没有cosmos的区块,也就没有区块链之说。 +
+验证人可以由普通用户通过质押Atom来成为验证人,当然也可以接受别人的委托,这在上文已经分析过了,验证人由其总股权来决定即质押股权最多的前一百人会成为Cosmos的验证人。在Cosmos网络中,验证人的上限是一百,然后每年增长约在百分之十三,最终稳定在三百人左右。 +
+如果验证人胡作非为或者经常不在线,又或者没有参与到治理,那么他的相关的抵押的Atom会被Slash掉。Slash的数量根据具体的情况来决定。 +
+做为验证人,对硬件的要求有一定的限制,其实这和EOS的超级节点有些类似,毕竟做为一个验证人节点,没事就下线,被罚钱也不是什么好事,所以还是需要有一定的环境基础做保证。做为什么一个验证人的要求的细节,大家可以去官网查找相关的资料,这玩意目前还不能算多靠谱。 +
+ +## 二、相关的术语 +
+1、验证人:Cosmos Hub基于Tendermint,它由一组(100~300)个验证人来保证网络的安全。验证人负责去运行一个全节点,广播经过验证人私钥签名过的加密信息来参与共识。验证人最终根据投票的结果来决定新的区块并因此得到奖励。 +
+2、股权抵押:Cosmos Hub是一个POS的区块链,意味着验证人的权重由其提供质押的Atom的数量决定。形象的来说,谁抵押的钱多,入的股多,权力就越大,大到在top100时,就成了验证人。 +
+3、全节点:全节点就是能够完成区块链的所有功能的节点。相对应的还有轻节点,它处理区块头和小部分的交易。 +
+ +## 三、成为验证人的过程 +
+网络中的节点均可以发送一笔"declare-candidacy"交易用来表示他们想成为一个验证人,同时必须填写以下参数: +验证人公钥、名称、验证人的网站(可选)、验证人的描述信息(可选)、初始佣金比例、最大佣金、佣金变化率、最小自抵押数量、初始自抵押数量。 +如果某个节点参选,其它Atom持有者可以向其地址委托Atom,从而有效地向其股权池里增加股权。一个地址的所有股权是其自抵押的Atom和委托人委托的股权的总和。 +Top100的候选者被任命为验证人。如果某个验证人的股权总量跌出了前100名就会失去验证人权利,验证人的最大数量会依照计时按时间逐渐增加: +从第一年到最后一年的分布为100,113,127,144,163,184,208,235,265,300. +
+ +## 四、源码分析 +在x/stake的目录中,有验证人的源码(x/stake/validator.go): +
+ +``` golang +type Validator struct { + Owner sdk.Address `json:"owner"` // sender of BondTx - UnbondTx returns here + PubKey crypto.PubKey `json:"pub_key"` // pubkey of validator + Revoked bool `json:"revoked"` // has the validator been revoked from bonded status? + + PoolShares PoolShares `json:"pool_shares"` // total shares for tokens held in the pool + DelegatorShares sdk.Rat `json:"delegator_shares"` // total shares issued to a validator's delegators + + Description Description `json:"description"` // description terms for the validator + BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator + BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change + ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + Commission sdk.Rat `json:"commission"` // XXX the commission rate of fees charged to any delegators + CommissionMax sdk.Rat `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge + CommissionChangeRate sdk.Rat `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission + CommissionChangeToday sdk.Rat `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) + + // fee related + PrevBondedShares sdk.Rat `json:"prev_bonded_shares"` // total shares of a global hold pools +} + +// Validators - list of Validators +type Validators []Validator + +//委托人增加委托 +// XXX Audit this function further to make sure it's correct +// add tokens to a validator +func (v Validator) addTokensFromDel(pool Pool, + amount int64) (validator2 Validator, p2 Pool, issuedDelegatorShares sdk.Rat) { + + exRate := v.DelegatorShareExRate(pool) // bshr/delshr + + var poolShares PoolShares + var equivalentBondedShares sdk.Rat + switch v.Status() { + case sdk.Unbonded: + pool, poolShares = pool.addTokensUnbonded(amount) + case sdk.Unbonding: + pool, poolShares = pool.addTokensUnbonding(amount) + case sdk.Bonded: + pool, poolShares = pool.addTokensBonded(amount) + } + v.PoolShares.Amount = v.PoolShares.Amount.Add(poolShares.Amount) + equivalentBondedShares = poolShares.ToBonded(pool).Amount + + issuedDelegatorShares = equivalentBondedShares.Quo(exRate) // bshr/(bshr/delshr) = delshr + v.DelegatorShares = v.DelegatorShares.Add(issuedDelegatorShares) + + return v, pool, issuedDelegatorShares +} + +//stake.go +// validator for a delegated proof of stake system +//相关的验证人的接口函数,在上述的验证人结构中有体现 +type Validator interface { + GetStatus() BondStatus // status of the validator + GetOwner() Address // owner address to receive/return validators coins + GetPubKey() crypto.PubKey // validation pubkey + GetPower() Rat // validation power + GetBondHeight() int64 // height in which the validator became active +} +// properties for the set of all validators +type ValidatorSet interface { + // iterate through validator by owner-address, execute func for each validator + IterateValidators(Context, + func(index int64, validator Validator) (stop bool)) + + // iterate through bonded validator by pubkey-address, execute func for each validator + IterateValidatorsBonded(Context, + func(index int64, validator Validator) (stop bool)) + + Validator(Context, Address) Validator // get a particular validator by owner address + TotalPower(Context) Rat // total power of the validator set + Slash(Context, crypto.PubKey, int64, Rat) // slash the validator and delegators of the validator, specifying offence height & slash fraction + Revoke(Context, crypto.PubKey) // revoke a validator + Unrevoke(Context, crypto.PubKey) // unrevoke a validator +} +``` +
+创建验证人命令的源码: +
+ +``` golang +// create create validator command +func GetCmdCreateValidator(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "create-validator", + Short: "create new validator initialized with a self-delegation to it", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + + amount, err := sdk.ParseCoin(viper.GetString(FlagAmount)) + if err != nil { + return err + } + validatorAddr, err := sdk.GetAccAddressBech32(viper.GetString(FlagAddressValidator)) + if err != nil { + return err + } + + pkStr := viper.GetString(FlagPubKey) + if len(pkStr) == 0 { + return fmt.Errorf("must use --pubkey flag") + } + pk, err := sdk.GetValPubKeyBech32(pkStr) + if err != nil { + return err + } + if viper.GetString(FlagMoniker) == "" { + return fmt.Errorf("please enter a moniker for the validator using --moniker") + } + description := stake.Description{ + Moniker: viper.GetString(FlagMoniker), + Identity: viper.GetString(FlagIdentity), + Website: viper.GetString(FlagWebsite), + Details: viper.GetString(FlagDetails), + } + msg := stake.NewMsgCreateValidator(validatorAddr, pk, amount, description) + + // build and sign the transaction, then broadcast to Tendermint + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } + + cmd.Flags().AddFlagSet(fsPk) + cmd.Flags().AddFlagSet(fsAmount) + cmd.Flags().AddFlagSet(fsDescription) + cmd.Flags().AddFlagSet(fsValidator) + return cmd +} +``` +
+在Cosmos-SDK中,主要是提供了一些数据结构及相关的操作的验证人过程,而在Tendermint中则是提供了数据的具体的流动和通信接口。而在ABCI中提供了开发应用程序的接口和相关的协议。它是区块链和Tendermint的接口。只有通过它才可以接入相关的应用程序。 +
+这里提到了,在Tendermint中也包含有相关的验证人的部分: +
+//types/validator.go + +``` golang +// Volatile state for each Validator +// NOTE: The Accum is not included in Validator.Hash(); +// make sure to update that method if changes are made here +type Validator struct { + Address Address `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` + VotingPower int64 `json:"voting_power"` + + Accum int64 `json:"accum"` +} + +// RandValidator returns a randomized validator, useful for testing. +// UNSTABLE +func RandValidator(randPower bool, minPower int64) (*Validator, PrivValidator) { + privVal := NewMockPV() + votePower := minPower + if randPower { + votePower += int64(cmn.RandUint32()) + } + val := NewValidator(privVal.GetPubKey(), votePower) + return val, privVal +} + + +``` +
+ +//state/validation.go +
+它有一个重要的工作-验证块: +
+ +``` golang +// Validate block +//验证块 +func validateBlock(stateDB dbm.DB, s State, b *types.Block) error { + // validate internal consistency + if err := b.ValidateBasic(); err != nil { + return err + } + + // validate basic info + if b.ChainID != s.ChainID { + return fmt.Errorf("Wrong Block.Header.ChainID. Expected %v, got %v", s.ChainID, b.ChainID) + } + if b.Height != s.LastBlockHeight+1 { + return fmt.Errorf("Wrong Block.Header.Height. Expected %v, got %v", s.LastBlockHeight+1, b.Height) + } + /* TODO: Determine bounds for Time + See blockchain/reactor "stopSyncingDurationMinutes" + + if !b.Time.After(lastBlockTime) { + return errors.New("Invalid Block.Header.Time") + } + */ + + // validate prev block info + if !b.LastBlockID.Equals(s.LastBlockID) { + return fmt.Errorf("Wrong Block.Header.LastBlockID. Expected %v, got %v", s.LastBlockID, b.LastBlockID) + } + newTxs := int64(len(b.Data.Txs)) + if b.TotalTxs != s.LastBlockTotalTx+newTxs { + return fmt.Errorf("Wrong Block.Header.TotalTxs. Expected %v, got %v", s.LastBlockTotalTx+newTxs, b.TotalTxs) + } + + // validate app info + if !bytes.Equal(b.AppHash, s.AppHash) { + return fmt.Errorf("Wrong Block.Header.AppHash. Expected %X, got %v", s.AppHash, b.AppHash) + } + if !bytes.Equal(b.ConsensusHash, s.ConsensusParams.Hash()) { + return fmt.Errorf("Wrong Block.Header.ConsensusHash. Expected %X, got %v", s.ConsensusParams.Hash(), b.ConsensusHash) + } + if !bytes.Equal(b.LastResultsHash, s.LastResultsHash) { + return fmt.Errorf("Wrong Block.Header.LastResultsHash. Expected %X, got %v", s.LastResultsHash, b.LastResultsHash) + } + if !bytes.Equal(b.ValidatorsHash, s.Validators.Hash()) { + return fmt.Errorf("Wrong Block.Header.ValidatorsHash. Expected %X, got %v", s.Validators.Hash(), b.ValidatorsHash) + } + + // Validate block LastCommit. + if b.Height == 1 { + if len(b.LastCommit.Precommits) != 0 { + return errors.New("Block at height 1 (first block) should have no LastCommit precommits") + } + } else { + if len(b.LastCommit.Precommits) != s.LastValidators.Size() { + return fmt.Errorf("Invalid block commit size. Expected %v, got %v", + s.LastValidators.Size(), len(b.LastCommit.Precommits)) + } + err := s.LastValidators.VerifyCommit( + s.ChainID, s.LastBlockID, b.Height-1, b.LastCommit) + if err != nil { + return err + } + } + + // TODO: Each check requires loading an old validator set. + // We should cap the amount of evidence per block + // to prevent potential proposer DoS. + for _, ev := range b.Evidence.Evidence { + if err := VerifyEvidence(stateDB, s, ev); err != nil { + return types.NewEvidenceInvalidErr(ev, err) + } + } + + return nil +} +``` +
+在ABCI的相关软件中也定义了Validator这个数据结构: +
+ +``` golang +// Validator +type Validator struct { + Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + PubKey PubKey `protobuf:"bytes,2,opt,name=pub_key,json=pubKey" json:"pub_key"` + Power int64 `protobuf:"varint,3,opt,name=power,proto3" json:"power,omitempty"` +} + +``` +
+然后再看一个出块时调用的相关操作,它先是进行判断异常特别是双签,然后遍历验证人来对块进行签名。 +
+ +``` golang +// slashing begin block functionality +func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, sk Keeper) (tags sdk.Tags) { + // Tag the height + heightBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(heightBytes, uint64(req.Header.Height)) + tags = sdk.NewTags("height", heightBytes) + + // Deal with any equivocation evidence + for _, evidence := range req.ByzantineValidators { + pk, err := tmtypes.PB2TM.PubKey(evidence.Validator.PubKey) + if err != nil { + panic(err) + } + switch string(evidence.Type) { + case tmtypes.ABCIEvidenceTypeDuplicateVote: + sk.handleDoubleSign(ctx, evidence.Height, evidence.Time, pk) + default: + ctx.Logger().With("module", "x/slashing").Error(fmt.Sprintf("Ignored unknown evidence type: %s", string(evidence.Type))) + } + } + + // Iterate over all the validators which *should* have signed this block + for _, validator := range req.Validators { + present := validator.SignedLastBlock + pubkey, err := tmtypes.PB2TM.PubKey(validator.Validator.PubKey) + if err != nil { + panic(err) + } + sk.handleValidatorSignature(ctx, pubkey, present) + } + + return +} +``` +
+通过这些接口不断的定义相关的验证人的数据结构,在不同的状态下进行转换,来达到验证人在不同阶段的状态的控制,更详细的代码,因为项目未最终完成,不进一步的阐述。其中的细节还有很多,相关验证人部分的代码也在演进中。 + +
+
diff --git "a/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233Staking\346\250\241\345\235\227.md" "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233Staking\346\250\241\345\235\227.md" new file mode 100644 index 0000000..93dd070 --- /dev/null +++ "b/cosmos/cosmos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233Staking\346\250\241\345\235\227.md" @@ -0,0 +1,206 @@ +# cosmos源码分析之四Staking模块 + +## 一、术语 +在前面的分析中提到了Cosmos有几个重要的机制,其中一个就是Staking。在分析这个模块之前,需要把一些相关的术语说明一下: +
+Atom :Cosmsos原生权益代币(另外有一个费用代币Photon)。 +
+Atom holder:Atom代币所有者。 +
+Candidate:Validator的候选人,全节点的Atom代币所有者。 +
+Validator(验证人):由Candidate选举出来,负责对 Tendermint 共识中的消息进行签名。 +
+Delegator( 代理人):把自己的Atom代币交由其它Validator(或Candidate)的持有并拥有其权益。 +
+Bonding Atoms(绑定Atom):Atom锁定机制(使Atom受共识协议的控制),Atoms只能通过Validator或Candidate进行绑定,如果Validator作恶,那么他绑定的Atom就会受到损失。如果在一个解绑时限内没有被惩罚,Atom持有者就可以重获对他绑定的Atom的支配权。 +
+Unbonding period(解绑时限):从解绑操作到Atom持有者重获对这些Atom支配权的缓冲时间。 +
+Inflationary provisions(通货膨胀):Cosmos Hub周期性的创建Atom,并根据规则分发给绑定Atom的持有者,用于鼓励Atom的有者尽可能多的绑定他们的Atom代币。 +
+Transaction fees(交易费):指包含在一个Cosmos Hub的交易中的手续费,由Validator取得,并根据Validator和Delegator绑定的Atom数量进行分配。 +
+Commission fee (佣金):Validator为他所提供的服务从交易费中收取佣金。 +
+ +## 二、源码 +
+stake的源码在x/stake中,代码老大一片,稳住。 +
+ +``` golang +// Pool - dynamic parameters of the current state +type Pool struct { + LooseUnbondedTokens int64 `json:"loose_unbonded_tokens"` // tokens not associated with any validator + UnbondedTokens int64 `json:"unbonded_tokens"` // reserve of unbonded tokens held with validators + UnbondingTokens int64 `json:"unbonding_tokens"` // tokens moving from bonded to unbonded pool + BondedTokens int64 `json:"bonded_tokens"` // reserve of bonded tokens + UnbondedShares sdk.Rat `json:"unbonded_shares"` // sum of all shares distributed for the Unbonded Pool + UnbondingShares sdk.Rat `json:"unbonding_shares"` // shares moving from Bonded to Unbonded Pool + BondedShares sdk.Rat `json:"bonded_shares"` // sum of all shares distributed for the Bonded Pool + InflationLastTime int64 `json:"inflation_last_time"` // block which the last inflation was processed // TODO make time + Inflation sdk.Rat `json:"inflation"` // current annual inflation rate + + DateLastCommissionReset int64 `json:"date_last_commission_reset"` // unix timestamp for last commission accounting reset (daily) + + // Fee Related + PrevBondedShares sdk.Rat `json:"prev_bonded_shares"` // last recorded bonded shares - for fee calcualtions +} +type PoolShares struct { + Status sdk.BondStatus `json:"status"` + Amount sdk.Rat `json:"amount"` // total shares of type ShareKind +} + +// Validator defines the total amount of bond shares and their exchange rate to +// coins. Accumulation of interest is modelled as an in increase in the +// exchange rate, and slashing as a decrease. When coins are delegated to this +// validator, the validator is credited with a Delegation whose number of +// bond shares is based on the amount of coins delegated divided by the current +// exchange rate. Voting power can be calculated as total bonds multiplied by +// exchange rate. +type Validator struct { + Owner sdk.Address `json:"owner"` // sender of BondTx - UnbondTx returns here + PubKey crypto.PubKey `json:"pub_key"` // pubkey of validator + Revoked bool `json:"revoked"` // has the validator been revoked from bonded status? + + PoolShares PoolShares `json:"pool_shares"` // total shares for tokens held in the pool + DelegatorShares sdk.Rat `json:"delegator_shares"` // total shares issued to a validator's delegators + + Description Description `json:"description"` // description terms for the validator + BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator + BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change + ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + Commission sdk.Rat `json:"commission"` // XXX the commission rate of fees charged to any delegators + CommissionMax sdk.Rat `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge + CommissionChangeRate sdk.Rat `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission + CommissionChangeToday sdk.Rat `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) + + // fee related + PrevBondedShares sdk.Rat `json:"prev_bonded_shares"` // total shares of a global hold pools +} + +// keeper of the staking store +type Keeper struct { + storeKey sdk.StoreKey + cdc *wire.Codec + coinKeeper bank.Keeper + + // codespace + codespace sdk.CodespaceType +} +``` +
+上面列举了几个关键的数据结构,特别是最后一个,在前面分析时提到过,Keeper实际上是数据库上层抽象的一个数据结构体。在Cosmos中,Pool是整个全局状态的管理空间。它能够跟踪所有帐户持有的Atomic的状态,包括移动和通化膨胀信息等。 +
+也就是说它是一个Atom的集合,在Cosmos中有两个全局的pool,绑定池和解绑池。需要说明的是,这个Pool是一个逻辑上的概念,Share是Atom分配的一个单位,通过一些计算公式可以得到Atom,这个有一点类似于以太坊的Gas。但用途有些不同。当然,它的好处在于,可以用于非侵入式的修改相关者的Atom的数量,类似于一个动态的汇率管制机制。 +
+ +``` go +// equivalent amount of shares if the shares were bonded +func (s PoolShares) ToBonded(p Pool) PoolShares { + var amount sdk.Rat + switch s.Status { + case sdk.Bonded: + amount = s.Amount + case sdk.Unbonding: + exRate := p.unbondingShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr + amount = s.Amount.Mul(exRate) // ubshr*bshr/ubshr = bshr + case sdk.Unbonded: + exRate := p.unbondedShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr + amount = s.Amount.Mul(exRate) // ubshr*bshr/ubshr = bshr + } + return NewUnbondedShares(amount) +} + +//_________________________________________________________________________________________________________ + +// get the equivalent amount of tokens contained by the shares +func (s PoolShares) Tokens(p Pool) sdk.Rat { + switch s.Status { + case sdk.Bonded: + return p.unbondedShareExRate().Mul(s.Amount) // (tokens/shares) * shares + case sdk.Unbonding: + return p.unbondedShareExRate().Mul(s.Amount) + case sdk.Unbonded: + return p.unbondedShareExRate().Mul(s.Amount) + default: + panic("unknown share kind") + } +} +``` +
+对于代理人同样也适用这个算法来进行Atom的管理。 +
+ +``` go +// Delegation represents the bond with tokens held by an account. It is +// owned by one delegator, and is associated with the voting power of one +// pubKey. +// TODO better way of managing space +type Delegation struct { + DelegatorAddr sdk.Address `json:"delegator_addr"` + ValidatorAddr sdk.Address `json:"validator_addr"` + Shares sdk.Rat `json:"shares"` + Height int64 `json:"height"` // Last height bond updated +} +``` +
+通货膨胀也是Cosmos一个机制: +
+ +``` go +var hrsPerYrRat = sdk.NewRat(hrsPerYr) // as defined by a julian year of 365.25 days + +// process provisions for an hour period +func (k Keeper) processProvisions(ctx sdk.Context) Pool { + + pool := k.GetPool(ctx) + pool.Inflation = k.nextInflation(ctx) + + // Because the validators hold a relative bonded share (`GlobalStakeShare`), when + // more bonded tokens are added proportionally to all validators the only term + // which needs to be updated is the `BondedPool`. So for each previsions cycle: + + provisions := pool.Inflation.Mul(sdk.NewRat(pool.TokenSupply())).Quo(hrsPerYrRat).Evaluate() + pool.BondedTokens += provisions + return pool +} + +// get the next inflation rate for the hour +func (k Keeper) nextInflation(ctx sdk.Context) (inflation sdk.Rat) { + + params := k.GetParams(ctx) + pool := k.GetPool(ctx) + // The target annual inflation rate is recalculated for each previsions cycle. The + // inflation is also subject to a rate change (positive of negative) depending or + // the distance from the desired ratio (67%). The maximum rate change possible is + // defined to be 13% per year, however the annual inflation is capped as between + // 7% and 20%. + + // (1 - bondedRatio/GoalBonded) * InflationRateChange + inflationRateChangePerYear := sdk.OneRat().Sub(pool.bondedRatio().Quo(params.GoalBonded)).Mul(params.InflationRateChange) + inflationRateChange := inflationRateChangePerYear.Quo(hrsPerYrRat) + + // increase the new annual inflation for this next cycle + inflation = pool.Inflation.Add(inflationRateChange) + if inflation.GT(params.InflationMax) { + inflation = params.InflationMax + } + if inflation.LT(params.InflationMin) { + inflation = params.InflationMin + } + + return inflation.Round(precision) +} + +``` +
+它还是比较好理解的,增加一个系数来处理。 +
+ +## 三、总结 +
+Staking模块是主要的资金管理模块,具体的技术问题没有什么,主要还是一些规则的制定,明白了这些规则,在理解分析Cosmos时会省不少的时间。 +
diff --git "a/cosmos/cosmos\350\264\271\347\224\250\344\273\243\345\270\201photon.md" "b/cosmos/cosmos\350\264\271\347\224\250\344\273\243\345\270\201photon.md" new file mode 100644 index 0000000..1fa4bd6 --- /dev/null +++ "b/cosmos/cosmos\350\264\271\347\224\250\344\273\243\345\270\201photon.md" @@ -0,0 +1,37 @@ +# cosmos费用代码photon +## 一、介绍 +在Cosmos中,使用的代币是Atom,这个基本上都没有什么不了解。但是,在新的经济模型中,Cosmos又引入了一种费用代币,在前面提到过的,名字叫做Photon。基本上大家都明白的是,原生代币可以用来创建一整套围绕着区块链本身的经济模型的基础。比如以太坊,通过以太币,衍生出gas控制,挖矿机制等。同理,很容易理解Atom代币本身,但是Photon这个费用代币又是什么?他来干什么? +
+Photon是一种次级的费用代币,同样也是在Cosmos原生的。它的核心作用是用于交易。当然,它比Atom拥有更高的流动性。Photon,原意是不连续能量包的基本粒子,它可以携带调整的可见光。所以,干脆就叫光子(Photon)。 +
+Photon不会和其它代币一样进行公开发售,那么怎么样才能得到这个代币呢?网上的资料(https://medium.com/m/global-identity?redirectUrl=https%3A%2F%2Fblog.cosmos.network%2Fintroducing-the-hard-spoon-4a9288d3f0df)所言是通过一次以太坊的状态的hard spoon来分发这种代币。 +
+ +## 二、多代币和单一代币的比较 +## 1、单一代币 +在目前主流的区块链上,大多提单一代币的模式,比如比特币和以太坊。以以太坊为例,以太坊的代币既可以进行抵押也可以交易。但是,正如一个优点从某一个角度来看恰恰是一种缺点一样,单一代币简单明了,但是人们在其上面寄托了所有的功能的梦想。而在攻击者看来,这正好是一个弱点,他们只要从某一个角度切入进去,就可以顺理成章的把所有的坏事做到。当然,从理论上讲,这种风险不是没有被考虑过,但应该是觉得属于可控的范围。可是,风险终究是被放大了,至于放大到什么程度,放大到了人们是否可以忍受的地步,这个得经过时间的考验。 +
+ +## 2、多代币 +和单一代币正好相反,划分出专门来人事某一类功能的代币,会隔离开不同代币所代表的功能区间。正如Atom和Photon两种代币,前者单纯用来抵押,后者单纯用来交易。Cosmos通过生态系统将二者有机的结合起来。这样,对Cosmos网络的攻击从理论上会被缩小,由于Atom的流动性变差,其价格升高,导致任何人想垄断Atom付出的代价是得不尝失的。 +
+同样,在跨链中,不同的链对手续费的计价方式不同,这就导致了类似现实世界的汇率问题。而如果使用单纯的交易代币则可以减少这种汇率的风险。当然,这有一个前提,必须是使用Cosmos的网络。 +
+大家如果对此事情兴趣,可以阅读相关的文档: +
+https://drive.google.com/file/d/1jtyYtx7t1xy9gxEi2T5lXFNd8xUY7bhJ/view +
+## 3、Photon的分发使用 +Cosmos的生态中,可以使用其进行费用的转移交付。换句话说,任何在Cosmos生态中的产生的费用,都可以用其支付。Photon和其它区块链代币的产生类似,它以500/个以固定的比例生产出来。不过,这也是一种权衡的措施,因为它无法抵御通货膨胀。 +
+代币的分发,是通过复制整个以太坊的状态,这其中包括智能合约的状态,然后运行兼容的Ethermint的环境,将复制的状态(包括EVM)运行在Tendermint的共识上,这样在Cosmos网络启动时,以太坊的持有者,就可以要求获得Photon并将其转移到Cosmos的相关帐户上。需要说明的是,这个Photon获得的状态空间(Ethermint空间),只是一个获得代币的手段,它没有其它用处。 +
+ +![photon](img/Photon.jpg) +
+ +## 三、总结 +前面只是简单介绍了Photon这种代币和Cosmos的多代币的生态模式,但是Cosmos的演进速度太慢了,所以无法现在就说明哪种更好,哪种不好。这都需要进一步的看链的主体的开发的过程。但是,Cosmos的这种多代币的生态确实是给人们提供了一条新的思路。 +
+不断的创新才是区块链能够发展的前提,不管这种创新最终能否促进区块链的技术的提高。 +
diff --git a/cosmos/img/Consensus.png b/cosmos/img/Consensus.png new file mode 100644 index 0000000..4758809 Binary files /dev/null and b/cosmos/img/Consensus.png differ diff --git a/cosmos/img/Photon.jpg b/cosmos/img/Photon.jpg new file mode 100644 index 0000000..8aa5784 Binary files /dev/null and b/cosmos/img/Photon.jpg differ diff --git a/cosmos/img/consensus-0.jpg b/cosmos/img/consensus-0.jpg new file mode 100644 index 0000000..42eee0a Binary files /dev/null and b/cosmos/img/consensus-0.jpg differ diff --git a/cosmos/img/consensus-1.jpg b/cosmos/img/consensus-1.jpg new file mode 100644 index 0000000..74ff271 Binary files /dev/null and b/cosmos/img/consensus-1.jpg differ diff --git a/cosmos/img/cosmos-sdk.jpg b/cosmos/img/cosmos-sdk.jpg new file mode 100644 index 0000000..c8d9fae Binary files /dev/null and b/cosmos/img/cosmos-sdk.jpg differ diff --git a/cosmos/img/cosmos-sdk.png b/cosmos/img/cosmos-sdk.png new file mode 100644 index 0000000..2f5d186 Binary files /dev/null and b/cosmos/img/cosmos-sdk.png differ diff --git a/cosmos/img/cosmosSDK-inc.png b/cosmos/img/cosmosSDK-inc.png new file mode 100644 index 0000000..b303a5e Binary files /dev/null and b/cosmos/img/cosmosSDK-inc.png differ diff --git a/cosmos/img/cross-chain.jpg b/cosmos/img/cross-chain.jpg new file mode 100644 index 0000000..00baa3b Binary files /dev/null and b/cosmos/img/cross-chain.jpg differ diff --git a/cosmos/img/meitu-cross.jpg b/cosmos/img/meitu-cross.jpg new file mode 100644 index 0000000..0642b2c Binary files /dev/null and b/cosmos/img/meitu-cross.jpg differ diff --git a/cosmos/img/new-sdk.png b/cosmos/img/new-sdk.png new file mode 100644 index 0000000..a9c8927 Binary files /dev/null and b/cosmos/img/new-sdk.png differ diff --git a/cosmos/img/owner-sdk.png b/cosmos/img/owner-sdk.png new file mode 100644 index 0000000..2fe67ba Binary files /dev/null and b/cosmos/img/owner-sdk.png differ diff --git a/cosmos/img/tendermint-inc.png b/cosmos/img/tendermint-inc.png new file mode 100644 index 0000000..b1fd59f Binary files /dev/null and b/cosmos/img/tendermint-inc.png differ diff --git a/cosmos/img/tendermint.jpg b/cosmos/img/tendermint.jpg new file mode 100644 index 0000000..21ccb64 Binary files /dev/null and b/cosmos/img/tendermint.jpg differ diff --git a/cryptography/.gitkeep b/cryptography/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/eos/eos.block.data.structure.md b/eos/eos.block.data.structure.md new file mode 100644 index 0000000..9aa2c19 --- /dev/null +++ b/eos/eos.block.data.structure.md @@ -0,0 +1,245 @@ + + +# EOS Block Data Structure + + +## 整体数据结构定义图 + +我们先来看一张EOS整体的数据结构定义图 + +![](./img/eos_block_structure.png) + +从这个图上可以看出来,EOS在区块数据结构的定义上并不是特别复杂. + +## Block 定义 + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/block.hpp +struct signed_block : public signed_block_header { + using signed_block_header::signed_block_header; + signed_block() = default; + signed_block( const signed_block_header& h ):signed_block_header(h){} + + // 交易集合 + vector transactions; + extensions_type block_extensions; +}; +using signed_block_ptr = std::shared_ptr; + +// ..../eos/libraries/chain/include/eosio/chain/types.hpp +typedef vector>> extensions_type; +``` + +Block的定义使用的是signed_block struct,这个struct是从signed_block_header继承而来的,在这个结构体中的关键部分是包含了Transaction的vector。区块是由按顺序组织的交易来构成的集合。 + +block_extensions则定义了一系列的扩展信息,这些信息都由一个整数类型的code来定义,需要的时候,都可以根据这个整数code来解析相应的信息。 + +在这个结构体中包含的transaction都是使用transaction_receipt结构体,这个结构体又是从transaction_receipt_header继承而来,下面我们看看这个两个struct的定义。 + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/block.hpp +struct transaction_receipt_header { + // 定义交易状态的枚举类型 + enum status_enum { + // 这个表示执行成功(所以不需要执行出错逻辑) + executed = 0, + // 客观的来说,执行失败了(或者没有执行),某一个出错逻辑执行了 + soft_fail = 1, + // 执行失败了,并且执行的出错逻辑也失败了,所以并没有状态改变 + hard_fail = 2, + // 交易被延迟了,计划到未来的某个时间执行 + delayed = 3, + // 交易过期了,并且存储空间返还给用户 + expired = 4 ///< transaction expired and storage space refuned to user + }; + + // 状态数据 + fc::enum_type status; + // CPU使用情况 + uint32_t cpu_usage_us; + // 网络使用情况 + fc::unsigned_int net_usage_words; +}; + +struct transaction_receipt : public transaction_receipt_header { + fc::static_variant trx; +}; +``` + +transaction_receipt结构体主要包含了一个打包过的交易以及其对应的交易类型。其parent struct transaction_receipt_header则主要是记录了这个交易的状态信息,以及CPU和网络的使用情况。当一笔交易被某个区块引用时,区块生产者针对这笔交易会做出相应的操作,而操作的不同结果会导致这笔交易的不同状态. + +packed_transaction,顾名思义,就是把交易数据打包了,这个结构体里面还定义了,打包数据是否经过了压缩的标识信息, + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/transaction.hpp +struct packed_transaction { + // 定义打包数据是否压缩的枚举类型 + enum compression_type { + // 没有压缩 + none = 0, + // 使用zlib压缩 + zlib = 1, + }; + + // 签名信息 + vector signatures; + // 是否压缩的标识信息 + fc::enum_type compression; + // 上下文无关的信息 + bytes packed_context_free_data; + // 打包后的交易数据 + bytes packed_trx; +} +``` + +packed_transaction中打包的数据来自于signed_transaction结构体,这个结构体的主要作用就是对交易做签名。 + +signed_transaction又是从transaction结构体继承而来,一个transaction结构体的实例包含一系列的action,这些action要么全部成功,要么全部失败。 + +交易ID是通过对交易内容本身经过Hash运算得出,所以每个交易的ID是与其内容一一对应的。交易的主体是由操作构成的。一个交易在纳入区块之前必须含有签名,用以验证交易的合法性。 + +延迟型交易 + +交易分为两种类型:一种是账户发起的普通交易,一种是由代码生成的自动交易,自动交易可以设置一个延迟时间,这样的交易叫延迟型交易,这种交易不会立即被执行,而是等到设定时间到时才会被执行。 + + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/transaction.hpp +struct signed_transaction : public transaction +{ + // 签名信息 + vector signatures; + // 上下文无关的数据 + vector context_free_data; +}; + +struct transaction : public transaction_header { + // 上下文无关的action + vector context_free_actions; + // 交易操作 + vector actions; + // 交易扩展类型 + extensions_type transaction_extensions; +} +``` + +transaction_header结构体包含了与每一个交易相关联的固定大小的数据,这些数据从具体的交易数据中分离出来,可以在需要的时候,帮助解析交易数据,而不再需要更多的动态内存分配。 + +所有的交易都有一个期限,这个期限限定了一个交易必须在规定时间内被纳入区块链,如果我们发现一个交易的时限已经过去,就可以放心的放弃这个交易,因为所有生产者都不会将它纳入任何区块。 + + + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/transaction.hpp +struct transaction_header { + // 这个交易的过期时间 + time_point_sec expiration; + // 在最后的2^16 blocks中指定一个具体的block number + uint16_t ref_block_num = 0U; + // 在指定的get_ref_blocknum的blockid中取低位的32bit + uint32_t ref_block_prefix = 0UL; + // 最大的网络带块 + fc::unsigned_int max_net_usage_words = 0UL; + // 最大的CPU使用 + uint8_t max_cpu_usage_ms = 0; + // 这个交易的延期时间 + fc::unsigned_int delay_sec = 0UL; +} +``` + + +signed_block结构体是从signed_block_header继承而来的,这个signed_block_header结构体只是包含了一条数据,那就是producer的签名。 + +signed_block_header结构体又是从block_header继承而来的,这个结构体就包含了一个block中很多重要的数据,包括,时间戳,producer的名字,所有交易的merkle root,所有action的root等信息。 + +```c++ +// ..../eos/libraries/chain/include/eosio/chain/block_header.hpp +struct block_header +{ + block_timestamp_type timestamp; + account_name producer; + uint16_t confirmed = 1; + + block_id_type previous; + + checksum256_type transaction_mroot; + checksum256_type action_mroot; + + uint32_t schedule_version = 0; + optional new_producers; + extensions_type header_extensions; +}; + +struct signed_block_header : public block_header +{ + signature_type producer_signature; +}; +``` + +## Action(操作) + +在前面的transaction结构体中,我们看到包含了有action,这里我们对action做一下说明。 + +我们先看看action的数据结构定义: + +```c++ +// ..../eos/contracts/eosiolib/action.hpp +struct action { + // 账户:操作的来源 + account_name account; + // 名称:操作的标识 + action_name name; + // 授权:执行操作的许可列表 + vector authorization; + // 数据:执行操作需要用到的信息 + bytes data; +} +``` + +EOS区块链中的交易是由一个个操作(action)组成的,操作可以理解成一个能够更改区块链全局状态的方法,操作的顺序是确定的,一个交易内的操作要么全部执行成功,要么都不执行,这与交易的本意是一致的。操作是区块链的最底层逻辑,相当于区块链这个大脑的神经元,区块链的智能最终也是通过一个个操作的组合来实现的。 + +#### 操作的设计原则 + +- 独立原则 操作本身须包含足以解释操作含义的信息,而不需要依赖区块链提供的上下文信息来帮助解释。所以,即便一个操作的当前状态可以通过区块链上的数据推导得出,我们也需要将状态信息纳入操作数据中,以便每个操作是容易理解的。这个原则体现的是区块的可解释性,这一点非常重要,这个底层的设计原则将影响整个区块链的使用效率 +- 余额可计算原则 一个账户当前的余额计算,仅仅依赖于与这个账户相关的信息便可得出,而不需要解析整个区块链才能获得。这个原则针对的是比特币的设计,由于比特币的余额计算需要扫描区块链中的所有交易才能精准的计算出一个账户的余额,这使得一个非常基础的计算落地起来都变得相当繁琐,EOS的这个设计目的在于提升运算效率。 +- 明确费用原则 区块链的交易费用随时间变化而变化,所以,一个签名过的交易须明确的认同这个交易所需要支付的费用,这个费用是在交易形成之前就已经设定并且明确好了的,这一点也非常重要,因为明确的费用协议才能保证余额的正确计算。 +- 明确授权原则 每个操作须包含足够的授权信息以标明是哪一个账户拥有授权这个操作的权力,这种明确授权的设计思路带来几个好处: + - 便于集中管理 + - 可以优化授权管理 + - 便于并行处理 +- 关联账户原则 每个操作须包含足够的关联账户信息,以保证这个操作能够遍历所有相关联的账户,也就是这个操作能够影响的所有账户,这个原则的目的同样是为了确保账户的余额能够得到及时和准确的运算 + +#### 操作的来源 + +一个操作可以通过两种途径产生: +- 由一个账号产生,通过签名来授权,即显性方式。 +- 由代码生成,即隐形方式。 + + +操作的设计遵循React Flux设计模式,就是每一个操作将会被赋予一个名称,然后被分发给一个或者多个handler。在EOS环境中,每个操作对应的handler是通过scope和name来定义的,默认的handler也可以再次将操作分发给下一级的多个handler。所以,每个EOS应用可以实现自己的handler,当操作被分发到这个应用时,相应的handler的代码就会被执行。 + +操作的设计思路中另一重要概念是授权。每一个操作的执行都必须确保具备了指定账户的授权。授权通过许可(permission)的方式声明,对于一个授权动作,账户可以要求任意数量的许可,许可的验证是独立于代码执行的,只有所有规定的许可被成功验证之后,对应的代码才能够被执行。安全特性深深的嵌入了区块链的底层设计逻辑,同时又不让安全机制成为性能和结构的累赘,让它自成体系,独立管理。 + + + +## 区块的存储 - 区块日志 + +区块日志是存储区块的二进制文件,区块日志的特性是只能从末尾追加(append only),区块日志包含两类文件: + +#### 区块文件,结构如下: ++---------+----------------+---------+----------------+-----+------------+-------------------+ + +| Block 1 | Pos of Block 1 | Block 2 | Pos of Block 2 | ... | Head Block | Pos of Head Block | + ++---------+----------------+---------+----------------+-----+------------+-------------------+ + +区块文件包含区块的内容以及每个区块的位置信息。区块位置信息是固定的8字节宽度,这样便于在连续读取区块的时候,按照读一个区块,向后跳跃8个字节,读一个区块,向后跳跃8个字节的模式快速加载区块内容。 + +#### 索引文件,结构如下: ++----------------+----------------+-----+-------------------+ + +| Pos of Block 1 | Pos of Block 2 | ... | Pos of Head Block | + ++----------------+----------------+-----+-------------------+ + +区块索引的目的在于提供一个基于区块序号的快速随机搜索方法,使用索引文件可以快速定位目标区块在区块文件中的具体位置。索引文件不是必须的,没有索引文件区块链仍然可以运行,索引文件的主要作用是通过少量空间换取速度提升。索引文件可以通过顺序读取区块文件来重新构建。 diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\225\264\344\275\223\344\273\213\347\273\215.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\225\264\344\275\223\344\273\213\347\273\215.md" new file mode 100644 index 0000000..f0c5e47 --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\346\225\264\344\275\223\344\273\213\347\273\215.md" @@ -0,0 +1,279 @@ +# EOS整体介绍 + +
+ +## 一、EOS的插件式设计 + +
+EOS中,虽然编程的复杂度和设计较比特币大幅提高,但其核心的思想其实并没有多大改变,目前来看,仍然以BOOST的signal,boost::asio的信号消息机制来完成模块间的解耦。相比比特币来言,做得更优雅,封装也更良好。 +
+先看一下插件设计的整体类图: +
+ +![plugin-pic](img/plugin_pic.png) +
+从上面的类图可以清楚的看到,整个插件的依赖和传导机制。然后在下面的流程分析中会详细说明一下它的具体的应用。 + +## 二、EOS的整体流程 +
+EOS的版本做了一次比较大的更迭,至少从形式上看是,它的生成路径下,完成了以下几个目标: +
+cleos:客户端,用来处理和区块链通信。帐户钱包等的管理。 +
+eosio-abigen:二进制ABI的生成程序。 +
+eosio-launcher:简化了eosd节点跨局域网或者跨更宽泛的网络的分布。 +
+keosd:钱包和帐户的实现控制程序 +
+nodeos:核心的节点程序,这个和老版本不一样了,至少名字不一样了。 + +
+一般情况下会启动cleos调用keosd来创建帐户和钱包。然后通过启动nodeos来产生节点,进行通信并根据配置生成区块和验证。进入重点,直接看一下 nodeos的创建代码: +
+ +``` c++ +int main(int argc, char** argv) +{ + try { + app().set_version(eosio::nodeos::config::version); + auto root = fc::app_path(); + app().set_default_data_dir(root / "eosio/nodeos/data" ); + app().set_default_config_dir(root / "eosio/nodeos/config" ); + //这里直接初始化了四个插件 + if(!app().initialize(argc, argv)) + return -1; + initialize_logging(); + ilog("nodeos version ${ver}", ("ver", eosio::nodeos::config::itoh(static_cast(app().version())))); + ilog("eosio root is ${root}", ("root", root.string())); + app().startup(); + app().exec(); + } catch (const fc::exception& e) { + elog("${e}", ("e",e.to_detail_string())); + } catch (const boost::exception& e) { + elog("${e}", ("e",boost::diagnostic_information(e))); + } catch (const std::exception& e) { + elog("${e}", ("e",e.what())); + } catch (...) { + elog("unknown exception"); + } + return 0; +} + +``` +
代码看上去并不多,当然,比之比特币最新中的几行代码来看,还是要稍有复杂的感觉,但是还可以承受,不过,随后可能c++技能的消耗水平会极剧增加。忽略开前几行的相关文件配置直接进行初始化代码看看去。 +
+ +``` c++ + +template +bool initialize(int argc, char** argv) { + return initialize_impl(argc, argv, {find_plugin()...}); +} +``` +
没啥,一个向量的初始化。不过有一个变参模板,如果想深入学习的得去看看相关资料。 +
+ +``` c++ +bool application::initialize_impl(int argc, char** argv, vector autostart_plugins) { + set_program_options();//设置命令选项 + + bpo::variables_map options;//声明保存结果变量 + bpo::store(bpo::parse_command_line(argc, argv, my->_app_options), options);//分析参数并保存 + + if( options.count( "help" ) ) { + cout << my->_app_options << std::endl; + return false; + } + + ...... + + //分析配置文件 + bpo::store(bpo::parse_config_file(config_file_name.make_preferred().string().c_str(), + my->_cfg_options, true), options); + + if(options.count("plugin") > 0) + { + auto plugins = options.at("plugin").as>(); + for(auto& arg : plugins) + { + vector names; + boost::split(names, arg, boost::is_any_of(" \t,")); + for(const std::string& name : names) + get_plugin(name).initialize(options);//分步初始化第一步,获取指定名插件并初始化,其它类同 + } + } + //下面是注册插件,并查寻依赖的相关插件,然后调用,并初始化 + for (auto plugin : autostart_plugins) + if (plugin != nullptr && plugin->get_state() == abstract_plugin::registered) + plugin->initialize(options);//分步初始化第一步,获取指定名插件并初始化,其它类同 + + bpo::notify(options);//更新最新参数至options + + return true; +} + +``` +
里面反复的参数控制代码略过了。里面主要是使用了BOOST的参数解析和更新机制 + +
+这里的调用很简单,其实就是从map里查找相关的插件,用类名和字符串,这里面用到了BOOST中的一些库boost::core::demangle(typeid(Plugin).name()),用来返回类型的名字。然后再用名字的字符串查找出插件。这里面有一个问题,为什么从plugins这个map中可以查找出对象,仔细看一下有些插件的CPP文件中会有类似的代码: +
+ +``` c++ +static appbase::abstract_plugin& _net_plugin = app().register_plugin(); +``` +
+静态注册了啊。但是有一些插件里没有啊,怎么回事儿?其实接着看代码就发现了问题所在。如下: +
+ +``` c++ +virtual void initialize(const variables_map& options) override { + if(\_state == registered) { + \_state = initialized; + //分步初始化,第二步 + static_cast(this)->plugin_requires([&](auto& plug){ plug.initialize(options); });//初始化此插件依赖的插件,并递归调用依赖插件 + static_cast(this)->plugin_initialize(options); //初始化插件 + //ilog( "initializing plugin ${name}", ("name",name()) ); + app().plugin_initialized(*this);//保存启动的插件 + } + assert(\_state == initialized); /// if initial state was not registered, final state cannot be initiaized +} +``` +
+plugin_requires,这个函数的定义就通过宏来产生了。 +
+ +``` C++ +//先看一个调用实现 +class chain_plugin : public plugin { +public: + APPBASE_PLUGIN_REQUIRES() +...... +}; +#define APPBASE_PLUGIN_REQUIRES_VISIT( r, visitor, elem ) \ + visitor( appbase::app().register_plugin() ); + +#define APPBASE_PLUGIN_REQUIRES( PLUGINS ) \ + template \ + void plugin_requires( Lambda&& l ) { \ + BOOST_PP_SEQ_FOR_EACH( APPBASE_PLUGIN_REQUIRES_VISIT, l, PLUGINS ) \ + } +//再看另外一个调用实现 +class producer_plugin : public appbase::plugin { +public: + APPBASE_PLUGIN_REQUIRES((chain_plugin)) +...... +}; + +``` +
+就这样,基础的插件和基础插件依赖的插件,就这么被一一加载初始化。 + +
+ +## 三、EOS的程序技术特点 + +
+ +### 1、使用了较多的宏,并配合BOOST库。 +
+ +在EOS的代码中,可以隐约看到类似MFC的代码实现机制,举一个例子: +
+ +``` C++ +#define FC_CAPTURE_AND_RETHROW( ... ) \ + catch( fc::exception& er ) { \ + FC_RETHROW_EXCEPTION( er, warn, "", FC_FORMAT_ARG_PARAMS(__VA_ARGS__) ); \ + } catch( const std::exception& e ) { \ + fc::exception fce( \ + FC_LOG_MESSAGE( warn, "${what}: ",FC_FORMAT_ARG_PARAMS(__VA_ARGS__)("what",e.what())), \ + fc::std_exception_code,\ + BOOST_CORE_TYPEID(decltype(e)).name(), \ + e.what() ) ; throw fce;\ + } catch( ... ) { \ + throw fc::unhandled_exception( \ + FC_LOG_MESSAGE( warn, "",FC_FORMAT_ARG_PARAMS(__VA_ARGS__)), \ + std::current_exception() ); \ + } + +FC_CAPTURE_AND_RETHROW( (t) ) +``` +
包括前面提到的递归调用插件化的宏定义,再通过上面的调用实现对比,基本上是以动态生成代码为主,在比特币也有类似的实现,但规模和应用要小得多。 +
+ +### 2、模板的使用普及化 +
+ +在工程代码上广泛使用了模板,看一下插件的例子: +
+ +``` c++ +template +class plugin : public abstract_plugin { + public: + plugin():\_name(boost::core::demangle(typeid(Impl).name())){} + virtual ~plugin(){} + + virtual state get_state()const override { ... } + virtual const std::string& name()const override { ... } + + virtual void register_dependencies() { +....... + } + + virtual void initialize(const variables_map& options) override { +...... + } + + virtual void startup() override { +...... + } + + virtual void shutdown() override { +...... + } + +...... +}; +``` +
+ +### 3、更深入的绑定使用了c++1X和BOOST +
+这个就非常明显了,试举一个简单的例子: +
+ +``` c++ +//c++11语法糖 +for (const auto& at: trx_trace.action_traces) { + for (const auto& auth: at.act.authorization) { + result.emplace_back(auth.actor); + } + + result.emplace_back(at.receiver); +} +//BOOST的网络通信 +using boost::asio::ip::tcp; +unique_ptr acceptor; +std::unique_ptr my; +void net_plugin::plugin_startup() { + if( my->acceptor ) { + my->acceptor->open(my->listen_endpoint.protocol()); + my->acceptor->set_option(tcp::acceptor::reuse_address(true)); + my->acceptor->bind(my->listen_endpoint); + my->acceptor->listen(); + ilog("starting listener, max clients is ${mc}",("mc",my->max_client_count)); + my->start_listen_loop(); + } + + my->chain_plug->chain().on_pending_transaction.connect( &net_plugin_impl::transaction_ready); + my->start_monitors(); + + for( auto seed_node : my->supplied_peers ) { + connect( seed_node ); + } +} +``` +
目前初步看来,EOS对BOOST和c++14的依赖更深。 diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203\351\222\261\345\214\205\345\222\214\345\270\220\346\210\267.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203\351\222\261\345\214\205\345\222\214\345\270\220\346\210\267.md" new file mode 100644 index 0000000..186ec46 --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\203\351\222\261\345\214\205\345\222\214\345\270\220\346\210\267.md" @@ -0,0 +1,414 @@ +# eos源码分析之七钱包和帐户 + +## 一、EOS的钱包帐户 +EOS的钱包其实主要就是管理密钥对,因为他不负责产生地址,也就是说,不会像以前的以太坊或者比特币,要通过密钥来产生钱包地址。它主要是提供对帐户的签名管理,也就是前面说的签名需要的密钥进行管理。 +EOS使用是非UTXO机制,即帐户机制,这点和以太坊相同,但是他们又有不同之处,EOS为了使用安全方便,引入了权限和角色的功能。通过不同的帐户和私钥进行组合,可以达到创建不同的权限的帐户动作。举一个例子,你可以把你自己的帐户处理动作分配给任意的人,那么那个人就拥有了你的所有的帐户动作,但是它仍然是使用自己的密钥对来对你分配的动作进行签名。 +要创建帐户,首先要创建钱包,因为创建帐户需要创建钱包时产生的密钥对。 +
+ +``` c++ +//创建钱包 +string wallet_name = "default"; +auto createWallet = wallet->add_subcommand("create", localized("Create a new wallet locally"), false); +createWallet->add_option("-n,--name", wallet_name, localized("The name of the new wallet"), true); +createWallet->set_callback([&wallet_name] { + // wait for keosd to come up + try_port(uint16_t(std::stoi(parse_url(wallet_url).port)), 2000); + + const auto& v = call(wallet_url, wallet_create, wallet_name); + std::cout << localized("Creating wallet: ${wallet_name}", ("wallet_name", wallet_name)) << std::endl; + std::cout << localized("Save password to use in the future to unlock this wallet.") << std::endl; + std::cout << localized("Without password imported keys will not be retrievable.") << std::endl; + std::cout << fc::json::to_pretty_string(v) << std::endl; +}); +//因为创建帐户需要创建钱包时产生的密钥对 +// create key + create->add_subcommand("key", localized("Create a new keypair and print the public and private keys"))->set_callback( [](){ + auto pk = private_key_type::generate(); + auto privs = string(pk); + auto pubs = string(pk.get_public_key()); + std::cout << localized("Private key: ${key}", ("key", privs) ) << std::endl; + std::cout << localized("Public key: ${key}", ("key", pubs ) ) << std::endl; + }); +//创建帐户 +struct create_account_subcommand { + string creator; + string account_name; + string owner_key_str; + string active_key_str; + string stake_net; + string stake_cpu; + uint32_t buy_ram_bytes_in_kbytes = 0; + string buy_ram_eos; + bool transfer; + bool simple; + + create_account_subcommand(CLI::App* actionRoot, bool s) : simple(s) { + auto createAccount = actionRoot->add_subcommand( (simple ? "account" : "newaccount"), localized("Create an account, buy ram, stake for bandwidth for the account")); + createAccount->add_option("creator", creator, localized("The name of the account creating the new account"))->required(); + createAccount->add_option("name", account_name, localized("The name of the new account"))->required(); + //这里需要两个KEY + createAccount->add_option("OwnerKey", owner_key_str, localized("The owner public key for the new account"))->required(); + createAccount->add_option("ActiveKey", active_key_str, localized("The active public key for the new account")); + +...... + + add_standard_transaction_options(createAccount); + + createAccount->set_callback([this] { + if( !active_key_str.size() ) + active_key_str = owner_key_str; + public_key_type owner_key, active_key; + try { + owner_key = public_key_type(owner_key_str); + } EOS_RETHROW_EXCEPTIONS(public_key_type_exception, "Invalid owner public key: ${public_key}", ("public_key", owner_key_str)); + try { + active_key = public_key_type(active_key_str); + } EOS_RETHROW_EXCEPTIONS(public_key_type_exception, "Invalid active public key: ${public_key}", ("public_key", active_key_str)); + auto create = create_newaccount(creator, account_name, owner_key, active_key);//创建一个帐户 +....... + }); + } +}; +chain::action create_newaccount(const name& creator, const name& newaccount, public_key_type owner, public_key_type active) { + return action { + tx_permission.empty() ? vector{{creator,config::active_name}} : get_account_permissions(tx_permission), + eosio::chain::newaccount{//调用帐户创建 + .creator = creator, + .name = newaccount, + .owner = eosio::chain::authority{1, {{owner, 1}}, {}}, + .active = eosio::chain::authority{1, {{active, 1}}, {}} + } + }; +} +``` +
+整体的步骤来说就是创建钱包,创建密钥,导入密钥到钱包,由密钥来创建帐户。看代码中还有一个直接在钱包中创建密钥的命令。 +
+旧的帐户的管理在插件account_history_plugin中。它提供了一个接口插件account_history_api_plugin用来更方便的管理帐户的历史记录。同样,在历史记录的类管理里中,使用了account_history_plugin_impl类来真正提供历史记录的控制。 +
+但是在新的版本中,用history_plugin替代了它,相应的接口也替换成了history_api_plugin.这里面主要涉及到了以下几个类(排除api接口类):history_plugin_impl,这个类是真正的操作数据的类,所有的关于历史记录的动作,最终都要落在这个类中。history_plugin是插件增加的实体类,是调用history_plugin_impl的入口点。read_only类是真正处理数据的类。 +
+这里看一个帐户的交易记录读取: +
+ +``` c++ +read_only::get_transaction_result read_only::get_transaction( const read_only::get_transaction_params& p )const { + auto& chain = history->chain_plug->chain();//获得当前指定的Controller + + get_transaction_result result; + + result.id = p.id; + result.last_irreversible_block = chain.last_irreversible_block_num(); + + const auto& db = chain.db();//获得当前数据库的句柄 + + //得到并处理multiindex的结果 + const auto& idx = db.get_index(); + auto itr = idx.lower_bound( boost::make_tuple(p.id) ); + if( itr == idx.end() ) { + return result; + } + result.id = itr->trx_id; + result.block_num = itr->block_num; + result.block_time = itr->block_time; + + if( fc::variant(result.id).as_string().substr(0,8) != fc::variant(p.id).as_string().substr(0,8) ) + return result; + + //处理事务action内容 + while( itr != idx.end() && itr->trx_id == result.id ) { + + fc::datastream ds( itr->packed_action_trace.data(), itr->packed_action_trace.size() ); + action_trace t; + fc::raw::unpack( ds, t ); + result.traces.emplace_back( chain.to_variant_with_abi(t) ); + + ++itr; + } + + //处理块 + auto blk = chain.fetch_block_by_number( result.block_num ); + if( blk == nullptr ) { // still in pending + auto blk_state = chain.pending_block_state(); + if( blk_state != nullptr ) { + blk = blk_state->block; + } + } + //得到交易内容 + if( blk != nullptr ) { + for (const auto &receipt: blk->transactions) { + if (receipt.trx.contains()) { + auto &pt = receipt.trx.get(); + auto mtrx = transaction_metadata(pt); + if (mtrx.id == result.id) { + fc::mutable_variant_object r("receipt", receipt); + r("trx", chain.to_variant_with_abi(mtrx.trx)); + result.trx = move(r); + break; + } + } else { + auto &id = receipt.trx.get(); + if (id == result.id) { + fc::mutable_variant_object r("receipt", receipt); + result.trx = move(r); + break; + } + } + } + } + + return result; +} + +//chainbase.hpp + +template +const generic_index& get_index()const +{ + CHAINBASE_REQUIRE_READ_LOCK("get_index", typename MultiIndexType::value_type); + typedef generic_index index_type; + typedef index_type* index_type_ptr; + assert( \_index_map.size() > index_type::value_type::type_id ); + assert( \_index_map[index_type::value_type::type_id] ); + return *index_type_ptr( \_index_map[index_type::value_type::type_id]->get() );//返回一个multiindex的容器指针 +} +``` +
+这个函数会在history_api_plugin.cpp中由: +
+ +``` c++ +void history_api_plugin::plugin_startup() { + ilog( "starting history_api_plugin" ); + auto ro_api = app().get_plugin().get_read_only_api(); + //auto rw_api = app().get_plugin().get_read_write_api(); + + app().get_plugin().add_api({ +// CHAIN_RO_CALL(get_transaction), + CHAIN_RO_CALL(get_actions), + CHAIN_RO_CALL(get_transaction), + CHAIN_RO_CALL(get_key_accounts), + CHAIN_RO_CALL(get_controlled_accounts) + }); +} +``` +
+提供HTTP的调用,并封装成JSON格式回传给相关调用方。 +
+ +## 二、帐户的权限和角色 +
+
+在前边创建帐户时提到了owner 和 active,它们的权限分别有一个值为1的阈值。owner 和 active 所绑定的 公钥 , 则分别有一个值为1的权重。阈值和权重是什么呢? +
+阈值是指操作的最小权限,而权重指权限量。简单的说明一下,比如打开保险柜的阈值是3,然后有三个角色权重:1,2,3.则3权重的可以自己直接打开。2和1权重的需要向其它两个角色申请,当权重总和>=3时,才可以打开。 +
+owner是自己的根本权限,可以用来授权给别人的权限。而active是被授予的相关的权限。网上举得例子比较好理解: +
+owner这个权限比作一扇门,打开这扇门需要一把正确的钥匙。 而 owner 所绑定的那个公钥 对应的那把私钥 就是正确的钥匙。那么二者到底有什么具体的关系和内容呢? +
+owner:啥都能干,还可以做冷备份。 +
+active:除了不能修改owner之外的所有权限。其它所有的权限都是基于active产生出来的。 +
+帐户的权限在EOS中功能相对来说是比较全的。在EOS中分为单签名帐户和多签名帐户。 +
+ +### 1、单签名帐户 +
+因此单签名账户就是权限的阈值和钥匙的权重都为1的一种账户类型。使用某个权限,只需要一把对应的私钥就行了. +
+ +``` c++ +struct newaccount { + account_name creator; + account_name name; + authority owner; + authority active; +...... +}; +``` +
+ +![单权限](img/roles-s.jpg) +
+单签名其实好理解,其实就是一句话,自己的事情自己干,当然,如果你授权给了别人,别人也可以干,不过不用二者合作,一个即可。 +
+ +### 2、多签名帐户 +
+多签名帐户其实就是一个权限绑定了多个帐户或者公钥。要想使用一个权限得需要大于1个以上的签名了。 +
+ +![多签名帐户](img/roles-m.jpg) +
+还是举一个例子,比如有一个权限可以从帐户转走一笔钱,转钱的权限阈值设定为3,有三个角色bob,alice,joe,他们对应的权重为2,2,3.那么joe自己就可以直接操作转钱,而bob,alice由于权重不足,只能二者互相合作或者去向joe申请合作。 +
+它对应到EOS的区块链上,其实就是对帐户的授权,比如某个智能合约需要权限才能操作,那么它会在执行前检查当前帐户的权限,如果不足,则直接退出。否则,完成。 +
+ +### 3、密钥的恢复 +
+在EOS中,有一个比较重要的特点就是被盗窃的密钥可以恢复,不会像比特币那样,密钥丢失后所有的一切都永远的消失在区块链中。不过恢复也不是没有条件的: +
+首先,使用任何30天内的owner权限的密钥和指定的合作伙伴才能恢复。 +
+其次,合作伙伴不参成任何日常交易。合作伙伴其实就是指你的关联帐户。 +
+最后,在恢复的过程中,也可以设置一些类似QQ的恢复机制中的问题机制。 +
+ +## 三、签名的验证 +
+既然前面提到了签名需要验证,分析一下验证的过程,从push_transcations中对比一下: +
+ +``` c++ +void apply_context::schedule_deferred_transaction( const uint128_t& sender_id, account_name payer, transaction&& trx, bool replace_existing ) { +...... + + if( !control.skip_auth_check() && !privileged ) { // Do not need to check authorization if replayng irreversible block or if contract is privileged + if( payer != receiver ) { + require_authorization(payer); /// uses payer's storage + } + + // if a contract is deferring only actions to itself then there is no need + // to check permissions, it could have done everything anyway. + bool check_auth = false; + for( const auto& act : trx.actions ) { + if( act.account != receiver ) { + check_auth = true; + break; + } + } + if( check_auth ) { + control.get_authorization_manager() + .check_authorization( trx.actions, + {}, + {{receiver, config::eosio_code_name}}, + delay, + std::bind(&transaction_context::checktime, &this->trx_context), + false + ); + } + } + + uint32_t trx_size = 0; + auto& d = control.db(); +...... + + trx_context.add_ram_usage( payer, (config::billable_size_v + trx_size) ); +} + +void apply_context::require_authorization(const account_name& account, + const permission_name& permission) { + for( uint32_t i=0; i < act.authorization.size(); i++ ) + if( act.authorization[i].actor == account ) { + if( act.authorization[i].permission == permission ) { + used_authorizations[i] = true; + return; + } + } + EOS_ASSERT( false, missing_auth_exception, "missing authority of ${account}/${permission}", + ("account",account)("permission",permission) ); +} + +void +authorization_manager::check_authorization( const vector& actions, + const flat_set& provided_keys, + const flat_set& provided_permissions, + fc::microseconds provided_delay, + const std::function& \_checktime, + bool allow_unused_keys + )const +{ + const auto& checktime = ( static_cast(\_checktime) ? \_checktime : \_noop_checktime ); + + auto delay_max_limit = fc::seconds( \_control.get_global_properties().configuration.max_transaction_delay ); + + auto effective_provided_delay = (provided_delay >= delay_max_limit) ? fc::microseconds::maximum() : provided_delay; + + auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, + \_control.get_global_properties().configuration.max_authority_depth, + provided_keys, + provided_permissions, + effective_provided_delay, + checktime + ); + + map permissions_to_satisfy; + + for( const auto& act : actions ) { + bool special_case = false; + fc::microseconds delay = effective_provided_delay; + + if( act.account == config::system_account_name ) { + special_case = true; + + if( act.name == updateauth::get_name() ) { + check_updateauth_authorization( act.data_as(), act.authorization ); + } else if( act.name == deleteauth::get_name() ) { + check_deleteauth_authorization( act.data_as(), act.authorization ); + } else if( act.name == linkauth::get_name() ) { + check_linkauth_authorization( act.data_as(), act.authorization ); + } else if( act.name == unlinkauth::get_name() ) { + check_unlinkauth_authorization( act.data_as(), act.authorization ); + } else if( act.name == canceldelay::get_name() ) { + delay = std::max( delay, check_canceldelay_authorization(act.data_as(), act.authorization) ); + } else { + special_case = false; + } + } + + for( const auto& declared_auth : act.authorization ) { + + checktime(); + + if( !special_case ) { + auto min_permission_name = lookup_minimum_permission(declared_auth.actor, act.account, act.name); + if( min_permission_name ) { // since special cases were already handled, it should only be false if the permission is eosio.any + const auto& min_permission = get_permission({declared_auth.actor, \*min_permission_name}); + EOS_ASSERT( get_permission(declared_auth).satisfies( min_permission, + \_db.get_index().indices() ), + irrelevant_auth_exception, + "action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", declared_auth)("min", permission_level{min_permission.owner, min_permission.name}) ); + } + } + + auto res = permissions_to_satisfy.emplace( declared_auth, delay ); + if( !res.second && res.first->second > delay) { // if the declared_auth was already in the map and with a higher delay + res.first->second = delay; + } + } + } + + // Now verify that all the declared authorizations are satisfied: + + // Although this can be made parallel (especially for input transactions) with the optimistic assumption that the + // CPU limit is not reached, because of the CPU limit the protocol must officially specify a sequential algorithm + // for checking the set of declared authorizations. + // The permission_levels are traversed in ascending order, which is: + // ascending order of the actor name with ties broken by ascending order of the permission name. + for( const auto& p : permissions_to_satisfy ) { + checktime(); // TODO: this should eventually move into authority_checker instead + EOS_ASSERT( checker.satisfied( p.first, p.second ), unsatisfied_authorization, + "transaction declares authority '${auth}', " + "but does not have signatures for it under a provided delay of ${provided_delay} ms", + ("auth", p.first)("provided_delay", provided_delay.count()/1000) + ("delay_max_limit_ms", delay_max_limit.count()/1000) + ); + + } + + if( !allow_unused_keys ) { + EOS_ASSERT( checker.all_keys_used(), tx_irrelevant_sig, + "transaction bears irrelevant signatures from these keys: ${keys}", + ("keys", checker.unused_keys()) ); + } +} +``` +
+不过上面的英文注释很搞笑,说其实不必检查,这也是有谁没谁的了。在controller.cpp中push_transcation中也有类似的调用,可以对比分析。这样的情况下基本上帐户和钱包也就分析的差不多了。 +
diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\344\272\244\346\230\223.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\344\272\244\346\230\223.md" new file mode 100644 index 0000000..7763910 --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\344\272\244\346\230\223.md" @@ -0,0 +1,698 @@ +# eos源码分析之三交易 + +## 一、交易的介绍 + +
+ +***说明:最新的1.0及以上代码中已经移除了相关的cycle和shared部分*** + +
+基本上区块链都是这几板斧,又轮到交易了。一般交易都是最复杂的部分,因为它涉及到网络,帐户,数据库,共识,内存池等等好多部分。EOS的交易数据结构主要有两种: +
+ +1、signed_transaction:用户发起的交易 + +
+ +2、deferred_transaction:延期交易,注释的说明这种交易可以返回错误给当事人。 + +
+ +EOS为了应对海量的交易,引入分片(shard)技术,即在区块中维护了一条私有区块链,将一个block分割成多个cycle(循环),每个cycle的生成时间很短,而且不用等待完整的block确认完成(3秒),生成后直接异步广播发送,这样,交易在很快就被确认了。在一个cycle中,如果存在有大量互不想干的交易,但么多核多线程技术将极大的提高交易的处理速度 。 +这次仍然从客户端发起一笔交易开始来查看整个代码的流程,基本上是cleos发出Transaction到nodeos的HTTP接口,接口接收到消息,分析打包。 + +## 二、交易的具体过程 + +1、客户端发起交易: +
+无论是send_actions 还是 send_transaction最终都落到push_transaction +
+ +``` c++ +fc::variant push_transaction( signed_transaction& trx, int32_t extra_kcpu = 1000, packed_transaction::compression_type compression = packed_transaction::none ) { + auto info = get_info(); + trx.expiration = info.head_block_time + tx_expiration; + trx.set_reference_block(info.head_block_id); + + if (tx_force_unique) { + trx.context_free_actions.emplace_back( generate_nonce() ); + } + + auto required_keys = determine_required_keys(trx); + size_t num_keys = required_keys.is_array() ? required_keys.get_array().size() : 1; + + trx.max_kcpu_usage = (tx_max_cpu_usage + 1023)/1024; + trx.max_net_usage_words = (tx_max_net_usage + 7)/8; + + if (!tx_skip_sign) { + //生成交易 + sign_transaction(trx, required_keys); + } + + if (!tx_dont_broadcast) { + //调用HTTP接口,packed_transaction打包交易 + return call(push_txn_func, packed_transaction(trx, compression)); + } else { + return fc::variant(trx); + } +} +void sign_transaction(signed_transaction& trx, fc::variant& required_keys) { + // TODO determine chain id + fc::variants sign_args = {fc::variant(trx), required_keys, fc::variant(chain_id_type{})}; + const auto& signed_trx = call(wallet_host, wallet_port, wallet_sign_trx, sign_args); + trx = signed_trx.as(); +} +``` +
+看到调用call,那么进去: +
+ +``` c++ +template +fc::variant call( const std::string& server, uint16_t port, + const std::string& path, + const T& v ) { return eosio::client::http::call( server, port, path, fc::variant(v) ); } + +template +fc::variant call( const std::string& path, + const T& v ) { return eosio::client::http::call( host, port, path, fc::variant(v) ); } +//最终调用 +fc::variant call( const std::string& server, uint16_t port, + const std::string& path, + const fc::variant& postdata ) { +try { +...... + + while( endpoint_iterator != end ) { + // Try each endpoint until we successfully establish a connection. + tcp::socket socket(io_service); + try { + boost::asio::connect(socket, endpoint_iterator); + endpoint_iterator = end; + } + ...... + + // Form the request. We specify the "Connection: close" header so that the + // server will close the socket after transmitting the response. This will + // allow us to treat all data up until the EOF as the content. + //组建请求的流 + boost::asio::streambuf request; + std::ostream request_stream(&request); + request_stream << "POST " << path << " HTTP/1.0\r\n"; + request_stream << "Host: " << server << "\r\n"; + request_stream << "content-length: " << postjson.size() << "\r\n"; + request_stream << "Accept: */*\r\n"; + request_stream << "Connection: close\r\n\r\n"; + request_stream << postjson; + + // Send the request.发送组织好的Request + boost::asio::write(socket, request); + + // Read the response status line. The response streambuf will automatically + // grow to accommodate the entire line. The growth may be limited by passing + // a maximum size to the streambuf constructor. + //读取并处理Response + boost::asio::streambuf response; + boost::asio::read_until(socket, response, "\r\n"); + + // Check that response is OK.判断格式 + std::istream response_stream(&response); + std::string http_version; + response_stream >> http_version; + unsigned int status_code; + response_stream >> status_code; + std::string status_message; + std::getline(response_stream, status_message); + FC_ASSERT( !(!response_stream || http_version.substr(0, 5) != "HTTP/"), "Invalid Response" ); + + // Read the response headers, which are terminated by a blank line. + boost::asio::read_until(socket, response, "\r\n\r\n"); + + // Process the response headers.读取头 + std::string header; + while (std::getline(response_stream, header) && header != "\r") + { + // std::cout << header << "\n"; + } + // std::cout << "\n"; + + std::stringstream re; + // Write whatever content we already have to output. + if (response.size() > 0) + // std::cout << &response; + re << &response; + + // Read until EOF, writing data to output as we go.读取数据 + boost::system::error_code error; + while (boost::asio::read(socket, response, + boost::asio::transfer_at_least(1), error)) + re << &response; + +...... + } + + FC_ASSERT( !"unable to connect" ); +} FC_CAPTURE_AND_RETHROW() // error, "Request Path: ${server}:${port}${path}\nRequest Post Data: ${postdata}" , + // ("server", server)("port", port)("path", path)("postdata", postdata) ) +} +``` +
+在HTTP的名空间里定义了一大堆的API接口,这里用的是: +
+const string chain_func_base = "/v1/chain"; +
+const string push_txns_func = chain_func_base + "/push_transactions";(httpc.hpp中) +
+这里基本就相当于调用HTTP的接口了,也就是说cleos把相关的API调用及内容发到了服务端,也就是nodeos端。那就去服务端看看nodeos接收到请求后干了些什么,有什么具体的动作。上面使用的路径是chain, 所以到chain_api_plugin.cpp中去看。 +
+在前面的整体NODEOS启动时,会有插件启动这个函数执行: +
+ +``` c++ +//最终调用下面这个宏,这个宏展开稍微有一些复杂,其实就是名字替换和参数转换 类似struct get_account_params {name account_name;}; +#define CALL(api_name, api_handle, api_namespace, call_name, http_response_code) \ +{std::string("/v1/" #api_name "/" #call_name), \ + [this, api_handle](string, string body, url_response_callback cb) mutable { \ + try { \ + if (body.empty()) body = "{}"; \ + auto result = api_handle.call_name(fc::json::from_string(body).as()); \ + cb(http_response_code, fc::json::to_string(result)); \ + } \ + ......//去除异常 + }} +//用下面两个宏来展开增加的API +#define CHAIN_RO_CALL(call_name, http_response_code) CALL(chain, ro_api, chain_apis::read_only, call_name, http_response_code) +#define CHAIN_RW_CALL(call_name, http_response_code) CALL(chain, rw_api, chain_apis::read_write, call_name, http_response_code) +void chain_api_plugin::plugin_startup() { + ilog( "starting chain_api_plugin" ); + my.reset(new chain_api_plugin_impl(app().get_plugin().chain())); + auto ro_api = app().get_plugin().get_read_only_api(); + auto rw_api = app().get_plugin().get_read_write_api(); +//注册相关API + app().get_plugin().add_api({ + CHAIN_RO_CALL(get_info, 200), + CHAIN_RO_CALL(get_block, 200), + CHAIN_RO_CALL(get_account, 200), + CHAIN_RO_CALL(get_code, 200), + CHAIN_RO_CALL(get_table_rows, 200), + CHAIN_RO_CALL(get_currency_balance, 200), + CHAIN_RO_CALL(get_currency_stats, 200), + CHAIN_RO_CALL(abi_json_to_bin, 200), + CHAIN_RO_CALL(abi_bin_to_json, 200), + CHAIN_RO_CALL(get_required_keys, 200), + CHAIN_RW_CALL(push_block, 202), + CHAIN_RW_CALL(push_transaction, 202), + CHAIN_RW_CALL(push_transactions, 202) + }); +} +``` +
+上面的api_handle.call_name展开后是:read_write::push_transaction_results read_write::push_transaction(),在chain_plugin.cpp文件中。 +
+ +``` c++ +read_write::push_transaction_results read_write::push_transaction(const read_write::push_transaction_params& params) { + packed_transaction pretty_input; + auto resolver = make_resolver(this); + try { + abi_serializer::from_variant(params, pretty_input, resolver); + } EOS_RETHROW_EXCEPTIONS(chain::packed_transaction_type_exception, "Invalid packed transaction") + + auto result = db.push_transaction(pretty_input, skip_flags);//这行是根本,db定义为chain_controller +#warning TODO: get transaction results asynchronously + fc::variant pretty_output; + abi_serializer::to_variant(result, pretty_output, resolver); + return read_write::push_transaction_results{ result.id, pretty_output }; +} +``` +
+因为这个函数其实是调用的chain_controller的同名函数: +
+ +``` c++ +/** + * Attempts to push the transaction into the pending queue + * + * When called to push a locally generated transaction, set the skip_block_size_check bit on the skip argument. This + * will allow the transaction to be pushed even if it causes the pending block size to exceed the maximum block size. + * Although the transaction will probably not propagate further now, as the peers are likely to have their pending + * queues full as well, it will be kept in the queue to be propagated later when a new block flushes out the pending + * queues. + */ +transaction_trace chain_controller::push_transaction(const packed_transaction& trx, uint32_t skip) +{ try { + // If this is the first transaction pushed after applying a block, start a new undo session. + // This allows us to quickly rewind to the clean state of the head block, in case a new block arrives. + if( !\_pending_block ) { + _start_pending_block(); + } + + return with_skip_flags(skip, [&]() { + return \_db.with_write_lock([&]() { + return _push_transaction(trx); + }); + }); +} EOS_CAPTURE_AND_RETHROW( transaction_exception ) } +``` +
+看注释说得很清楚了, +
+如果是交易写入块后的第一个交易,是启动一个可撤销的Session,保证在新块到来时可以进行回滚: +
+ +``` c++ +void chain_controller::_start_pending_block( bool skip_deferred ) +{ + //配置等待块 + FC_ASSERT( !\_pending_block ); + \_pending_block = signed_block(); + \_pending_block_trace = block_trace(\*\_pending_block); + \_pending_block_session = \_db.start_undo_session(true); + \_pending_block->regions.resize(1); + \_pending_block_trace->region_traces.resize(1); + + _start_pending_cycle();//处理里块的cycle + _apply_on_block_transaction(); + _finalize_pending_cycle(); + + _start_pending_cycle(); + + if ( !skip_deferred ) { + _push_deferred_transactions( false ); + if (\_pending_cycle_trace && \_pending_cycle_trace->shard_traces.size() > 0 && \_pending_cycle_trace->shard_traces.back().transaction_traces.size() > 0) { + _finalize_pending_cycle(); + _start_pending_cycle(); + } + } +} +//下面的两个函数比较关键,一个是处理cycle,一个是添加交易,下面的英文注释说得也比较清楚 +/** + * Wraps up all work for current shards, starts a new cycle, and + * executes any pending transactions + */ +void chain_controller::_start_pending_cycle() { + // only add a new cycle if there are no cycles or if the previous cycle isn't empty + if (\_pending_block->regions.back().cycles_summary.empty() || + (!\_pending_block->regions.back().cycles_summary.back().empty() && + !\_pending_block->regions.back().cycles_summary.back().back().empty())) + \_pending_block->regions.back().cycles_summary.resize( \_pending_block->regions[0].cycles_summary.size() + 1 ); + + + \_pending_cycle_trace = cycle_trace(); + + \_pending_cycle_trace->shard_traces.resize(\_pending_cycle_trace->shard_traces.size() + 1 );//当前分片 + + auto& bcycle = \_pending_block->regions.back().cycles_summary.back(); + if(bcycle.empty() || !bcycle.back().empty()) + bcycle.resize( bcycle.size()+1 ); +} +void chain_controller::_apply_on_block_transaction() +{ + \_pending_block_trace->implicit_transactions.emplace_back(_get_on_block_transaction()); + transaction_metadata mtrx(packed_transaction(\_pending_block_trace->implicit_transactions.back()), get_chain_id(), head_block_time(), optional(), true /*is implicit*/); + _push_transaction(std::move(mtrx)); +} +//再处理一下deferred +vector chain_controller::_push_deferred_transactions( bool flush ) +{ + FC_ASSERT( \_pending_block, " block not started" ); + + if (flush && \_pending_cycle_trace && \_pending_cycle_trace->shard_traces.size() > 0) { + // TODO: when we go multithreaded this will need a better way to see if there are flushable + // deferred transactions in the shards + auto maybe_start_new_cycle = [&]() { + for (const auto &st: \_pending_cycle_trace->shard_traces) { + for (const auto &tr: st.transaction_traces) { + for (const auto &req: tr.deferred_transaction_requests) { + if ( req.contains() ) { + const auto& dt = req.get(); + if ( fc::time_point(dt.execute_after) <= head_block_time() ) { + // force a new cycle and break out + _finalize_pending_cycle(); + _start_pending_cycle(); + return; + } + } + } + } + } + }; + + maybe_start_new_cycle(); + } + } +``` +
+这里得重点看看下面这个函数: +
+ +``` c++ +void chain_controller::_finalize_pending_cycle() +{ + // prune empty shard + if (!\_pending_block->regions.back().cycles_summary.empty() && + !\_pending_block->regions.back().cycles_summary.back().empty() && + \_pending_block->regions.back().cycles_summary.back().back().empty()) { + \_pending_block->regions.back().cycles_summary.back().resize( \_pending_block->regions.back().cycles_summary.back().size() - 1 ); + \_pending_cycle_trace->shard_traces.resize(\_pending_cycle_trace->shard_traces.size() - 1 ); + } + // prune empty cycle + if (!\_pending_block->regions.back().cycles_summary.empty() && + \_pending_block->regions.back().cycles_summary.back().empty()) { + \_pending_block->regions.back().cycles_summary.resize( \_pending_block->regions.back().cycles_summary.size() - 1 ); + \_pending_cycle_trace.reset(); + return; + } + + for( int idx = 0; idx < \_pending_cycle_trace->shard_traces.size(); idx++ ) { + auto& trace = \_pending_cycle_trace->shard_traces.at(idx); + auto& shard = \_pending_block->regions.back().cycles_summary.back().at(idx); + + trace.finalize_shard(); + shard.read_locks.reserve(trace.read_locks.size()); + shard.read_locks.insert(shard.read_locks.end(), trace.read_locks.begin(), trace.read_locks.end()); + + shard.write_locks.reserve(trace.write_locks.size()); + shard.write_locks.insert(shard.write_locks.end(), trace.write_locks.begin(), trace.write_locks.end()); + } + + _apply_cycle_trace(*\_pending_cycle_trace); + \_pending_block_trace->region_traces.back().cycle_traces.emplace_back(std::move(*\_pending_cycle_trace)); + \_pending_cycle_trace.reset(); +} +``` +
+ +***这里遇到的问题是,没有找到Cycle的周期性增加,对块内的分片也因此不是非常清楚。*** +
+现在接着回到交易,看前面调用了_push_transaction, 它有两个重载,前面的重载会在函数内调用后面的重载函数,即: +
+ +``` c++ + +transaction_trace chain_controller::_push_transaction(const packed_transaction& packed_trx) +{ try { +...... + + // 根据情况来分别打包普通交易和延迟交易 + if( mtrx.delay.count() == 0 ) { + result = _push_transaction( std::move(mtrx) ); + } else { + + result = wrap_transaction_processing( std::move(mtrx), + [this](transaction_metadata& meta) { return delayed_transaction_processing(meta); } ); + } + + // notify anyone listening to pending transactions + //这个最终会调用connections的enqueue-queue_write-do_queue_write,然后发送广播消息 + on_pending_transaction(\_pending_transaction_metas.back(), packed_trx); + + \_pending_block->input_transactions.emplace_back(packed_trx);//插入到区块中 +...... + +} FC_CAPTURE_AND_RETHROW( (transaction_header(packed_trx.get_transaction())) ) } + +transaction_trace chain_controller::_push_transaction( transaction_metadata&& data ) +{ try { + auto process_apply_transaction = [this](transaction_metadata& meta) { +...... + /// TODO: move \_pending_cycle into db so that it can be undone if transation fails, for now we will apply + /// the transaction first so that there is nothing to undo... this only works because things are currently + /// single threaded + // set cycle, shard, region etc + meta.region_id = 0; + meta.cycle_index = cyclenum; + meta.shard_index = 0; + return _apply_transaction( meta );//交易打入块中 + }; + // wdump((transaction_header(data.trx()))); + return wrap_transaction_processing( move(data), process_apply_transaction ); +} FC_CAPTURE_AND_RETHROW( ) } +``` +
+经过上面的处理之后,最后通过_apply_transaction把交易最终打入块中: +
+ +``` c++ +//写入并执行交易 +transaction_trace chain_controller::__apply_transaction( transaction_metadata& meta ) +{ try { + transaction_trace result(meta.id); + + for (const auto &act : meta.trx().context_free_actions) { + apply_context context(\*this, \_db, act, meta); + context.context_free = true; + context.exec();//执行 + fc::move_append(result.action_traces, std::move(context.results.applied_actions)); + FC_ASSERT( result.deferred_transaction_requests.size() == 0 ); + } + + for (const auto &act : meta.trx().actions) { + apply_context context(\*this, \_db, act, meta); + context.exec(); + context.results.applied_actions.back().auths_used = act.authorization.size() - context.unused_authorizations().size(); + fc::move_append(result.action_traces, std::move(context.results.applied_actions)); + fc::move_append(result.deferred_transaction_requests, std::move(context.results.deferred_transaction_requests)); + } + + update_resource_usage(result, meta); + + update_permission_usage(meta); + record_transaction(meta.trx());//保存到数据库 + return result; +} FC_CAPTURE_AND_RETHROW() } + +transaction_trace chain_controller::_apply_transaction( transaction_metadata& meta ) { try { + auto execute = [this](transaction_metadata& meta) -> transaction_trace { + try { + auto temp_session = \_db.start_undo_session(true); + auto result = __apply_transaction(meta); +...... + } catch (...) { +...... + } + }; + +...... +} FC_CAPTURE_AND_RETHROW( (transaction_header(meta.trx())) ) } +``` +
+交易完成后,就需要打包到块并进行广播了.这里只简单说一下,在介绍区块和共识时再详细说明: +
+在producer_plugin插件中: +
+ +``` c++ +void producer_plugin::plugin_startup() +{ try { + ilog("producer plugin: plugin_startup() begin"); + chain::chain_controller& chain = app().get_plugin().chain(); + + if (!my->_producers.empty()) + { +...... + my->schedule_production_loop(); + } else +...... + } FC_CAPTURE_AND_RETHROW() } + + +void producer_plugin_impl::schedule_production_loop() { + //Schedule for the next second's tick regardless of chain state + // If we would wait less than 50ms (1/10 of block_interval), wait for the whole block interval. +...... + + //\_timer.expires_from_now(boost::posix_time::microseconds(time_to_next_block_time)); + \_timer.expires_from_now( boost::posix_time::microseconds(time_to_next_block_time) ); + //\_timer.async_wait(boost::bind(&producer_plugin_impl::block_production_loop, this)); + \_timer.async_wait( [&](const boost::system::error_code&){ block_production_loop(); } ); +} + +block_production_condition::block_production_condition_enum producer_plugin_impl::block_production_loop() { + block_production_condition::block_production_condition_enum result; + fc::mutable_variant_object capture; + try + { + result = maybe_produce_block(capture);//生产块 + } + catch( const fc::canceled_exception& ) + { + ...... + } + + if(result != block_production_condition::produced && result == \_prev_result) { + \_prev_result_count++; + } + else { + \_prev_result_count = 1; + \_prev_result = result; + switch(result) + { + case block_production_condition::produced: { + const auto& db = app().get_plugin().chain(); + auto producer = db.head_block_producer(); +...... + break; + } +...... + } + } + schedule_production_loop();//循环调用 + return result; +} + +block_production_condition::block_production_condition_enum producer_plugin_impl::maybe_produce_block(fc::mutable_variant_object& capture) { + chain::chain_controller& chain = app().get_plugin().chain(); + fc::time_point now = fc::time_point::now(); + + if (app().get_plugin().is_skipping_transaction_signatures()) { + \_production_skip_flags |= skip_transaction_signatures; + } + // If the next block production opportunity is in the present or future, we're synced. + if( \!\_production_enabled ) + { + if( chain.get_slot_time(1) >= now ) + \_production_enabled = true; + else + return block_production_condition::not_synced; + } + + // is anyone scheduled to produce now or one second in the future? + uint32_t slot = chain.get_slot_at_time( now ); + if( slot == 0 ) + { + capture("next_time", chain.get_slot_time(1)); + return block_production_condition::not_time_yet; + } + + // + // this assert should not fail, because now <= db.head_block_time() + // should have resulted in slot == 0. + // + // if this assert triggers, there is a serious bug in get_slot_at_time() + // which would result in allowing a later block to have a timestamp + // less than or equal to the previous block + // + assert( now > chain.head_block_time() ); + + auto scheduled_producer = chain.get_scheduled_producer( slot ); + // we must control the producer scheduled to produce the next block. + if( \_producers.find( scheduled_producer ) == \_producers.end() ) + { + capture("scheduled_producer", scheduled_producer); + return block_production_condition::not_my_turn; + } + + auto scheduled_time = chain.get_slot_time( slot ); + eosio::chain::public_key_type scheduled_key = chain.get_producer(scheduled_producer).signing_key; + auto private_key_itr = \_private_keys.find( scheduled_key ); + +...... + + //此处产生块 + auto block = chain.generate_block( + scheduled_time, + scheduled_producer, + private_key_itr->second, + \_production_skip_flags + ); + + capture("n", block.block_num())("t", block.timestamp)("c", now)("count",block.input_transactions.size())("id",string(block.id()).substr(8,8)); + + app().get_plugin().broadcast_block(block);//广播块消息 + return block_production_condition::produced; +} +``` +
+产生块的代码也比较简单,在chain-controller.cpp中: +
+ +``` c++ +signed_block chain_controller::generate_block( + block_timestamp_type when, + account_name producer, + const private_key_type& block_signing_private_key, + uint32_t skip /* = 0 */ + ) +{ try { + return with_skip_flags( skip | created_block, [&](){ + return \_db.with_write_lock( [&](){ + //直接调用同名函数 + return _generate_block( when, producer, block_signing_private_key ); + }); + }); +} FC_CAPTURE_AND_RETHROW( (when) ) } + +signed_block chain_controller::_generate_block( block_timestamp_type when, + account_name producer, + const private_key_type& block_signing_key ) +{ try { + + try { + //检测并获取相关参数值 + FC_ASSERT( head_block_time() < (fc::time_point)when, "block must be generated at a timestamp after the head block time" ); + uint32_t skip = \_skip_flags; + uint32_t slot_num = get_slot_at_time( when );//获取当前生产者的位置 + FC_ASSERT( slot_num > 0 ); + account_name scheduled_producer = get_scheduled_producer( slot_num );//获得当前区块的生产者 + FC_ASSERT( scheduled_producer == producer ); + + const auto& producer_obj = get_producer(scheduled_producer); + + //如果符合条件,创建一个未决的块 + if( !\_pending_block ) { + _start_pending_block(); + } + //完成Cycle的构建 + _finalize_pending_cycle(); + + if( !(skip & skip_producer_signature) ) + FC_ASSERT( producer_obj.signing_key == block_signing_key.get_public_key(), + "producer key ${pk}, block key ${bk}", ("pk", producer_obj.signing_key)("bk", block_signing_key.get_public_key()) ); + + //设置未决块转成正式块的相关参数 + \_pending_block->timestamp = when; + \_pending_block->producer = producer_obj.owner; + \_pending_block->previous = head_block_id(); + \_pending_block->block_mroot = get_dynamic_global_properties().block_merkle_root.get_root(); + \_pending_block->transaction_mroot = \_pending_block_trace->calculate_transaction_merkle_root(); + \_pending_block->action_mroot = \_pending_block_trace->calculate_action_merkle_root(); + + if( is_start_of_round( \_pending_block->block_num() ) ) { + auto latest_producer_schedule = _calculate_producer_schedule(); + if( latest_producer_schedule != _head_producer_schedule() ) + \_pending_block->new_producers = latest_producer_schedule; + } + \_pending_block->schedule_version = get_global_properties().active_producers.version; + + if( !(skip & skip_producer_signature) ) + \_pending_block->sign( block_signing_key ); + + //结束块并广播消息保存相关数据 + _finalize_block( *\_pending_block_trace, producer_obj ); + + \_pending_block_session->push(); + + auto result = move( *\_pending_block ); + + clear_pending(); + + if (!(skip&skip_fork_db)) { + \_fork_db.push_block(result);//加入到链中 + } + return result; + } catch ( ... ) { + clear_pending(); + + elog( "error while producing block" ); + _start_pending_block(); + throw; + } + +} FC_CAPTURE_AND_RETHROW( (producer) ) } +``` +
+交易基本就完成了,这块比较麻烦,而且有些细节其实资料和代码有此不匹配,留待后面继续解决。 + +
+
diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\347\275\221\347\273\234.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\347\275\221\347\273\234.md" new file mode 100644 index 0000000..c8dcc34 --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\347\275\221\347\273\234.md" @@ -0,0 +1,630 @@ +# eos源码分析之二网络 +
+ +# 一、网络的初始化和启动 +
+P2P网络是区块链的运行的基础模块,在EOS中,主要就是net_plugin,http_plugin,net_pai_plugin,当然在这个过程中网络也会引用到其它的一些模块的接口,但为了清晰,重点介绍网络相关部分,其它略过。 +
+首先看一下网络插件生成的时候的代码: +
+ +``` c++ +net_plugin::net_plugin() + :my( new net_plugin_impl ) { + my_impl = my.get();//此处写得不是太好,从智能指针又退化回到 普通指针 +} +``` +
+可以看到,它生成了一个net_plugin_impl的实例,真正的网络操作相关的代码其实在这个类中,看名字也可以明白,JAVA接口经常这么干。然后接着按Main函数中的初始化来看: +
+ +``` c++ +void net_plugin::plugin_initialize( const variables_map& options ) { +......//日志相关忽略 + + //初始化相关参数,版本,是否发送完整块,交易周期等 + my->network_version = static_cast(app().version()); + my->network_version_match = options.at("network-version-match").as(); + my->send_whole_blocks = def_send_whole_blocks; + + my->sync_master.reset( new sync_manager(options.at("sync-fetch-span").as() ) ); + my->big_msg_master.reset( new big_msg_manager ); + + my->connector_period = std::chrono::seconds(options.at("connection-cleanup-period").as()); + my->txn_exp_period = def_txn_expire_wait; + my->resp_expected_period = def_resp_expected_wait; + my->big_msg_master->just_send_it_max = def_max_just_send; + my->max_client_count = options.at("max-clients").as(); + + my->num_clients = 0; + my->started_sessions = 0; + + //使用BOOST的resolver来处理与网络相关的数据格式的转换 + my->resolver = std::make_shared( std::ref( app().get_io_service() ) ); + + //根据options设置来设置相关配置 + if(options.count("p2p-listen-endpoint")) { + my->p2p_address = options.at("p2p-listen-endpoint").as< string >(); + auto host = my->p2p_address.substr( 0, my->p2p_address.find(':') ); + auto port = my->p2p_address.substr( host.size()+1, my->p2p_address.size() ); + idump((host)(port)); + tcp::resolver::query query( tcp::v4(), host.c_str(), port.c_str() ); + // Note: need to add support for IPv6 too? + //得到监听地址 + my->listen_endpoint = \*my->resolver->resolve( query); + //重置boost socket网络接收器 + my->acceptor.reset( new tcp::acceptor( app().get_io_service() ) ); + } + if(options.count("p2p-server-address")) { + my->p2p_address = options.at("p2p-server-address").as< string >(); + } + else { + if(my->listen_endpoint.address().to_v4() == address_v4::any()) { + boost::system::error_code ec; + auto host = host_name(ec); + if( ec.value() != boost::system::errc::success) { + + FC_THROW_EXCEPTION( fc::invalid_arg_exception, + "Unable to retrieve host_name. ${msg}",( "msg",ec.message())); + + } + auto port = my->p2p_address.substr( my->p2p_address.find(':'), my->p2p_address.size()); + my->p2p_address = host + port; + } + } + ...... + //处理连接设置 + if(options.count("allowed-connection")) { + const std::vector allowed_remotes = options["allowed-connection"].as>(); + for(const std::string& allowed_remote : allowed_remotes) + { + if(allowed_remote == "any") + my->allowed_connections |= net_plugin_impl::Any; + else if(allowed_remote == "producers") + my->allowed_connections |= net_plugin_impl::Producers; + else if(allowed_remote == "specified") + my->allowed_connections |= net_plugin_impl::Specified; + else if(allowed_remote == "none") + my->allowed_connections = net_plugin_impl::None; + } + } +...... + //查找依赖的链插件 + my->chain_plug = app().find_plugin();//插件已经在上一篇中讲过的宏中注册 + my->chain_plug->get_chain_id(my->chain_id); + fc::rand_pseudo_bytes(my->node_id.data(), my->node_id.data_size()); + ilog("my node_id is ${id}",("id",my->node_id)); + //重置心跳定时器 + my->keepalive_timer.reset(new boost::asio::steady_timer(app().get_io_service())); + my->ticker(); +} +``` +
+初始化完成后,看一下启动的代码 +
+ +``` c++ +void net_plugin::plugin_startup() { + if( my->acceptor ) { + 常见的网络服务操作,打开监听服务,设置选项,绑定地址,启动监听 + my->acceptor->open(my->listen_endpoint.protocol()); + my->acceptor->set_option(tcp::acceptor::reuse_address(true)); + my->acceptor->bind(my->listen_endpoint); + my->acceptor->listen(); + ilog("starting listener, max clients is ${mc}",("mc",my->max_client_count)); + my->start_listen_loop();//循环接收连接 + } + + //绑定等待交易信号 + my->chain_plug->chain().on_pending_transaction.connect( &net_plugin_impl::transaction_ready); + my->start_monitors();//启动连接和交易到期的监视(一个自循环) + + for( auto seed_node : my->supplied_peers ) { + connect( seed_node );//连接种子节点,接入P2P网络 + } +} +``` +
+代码看上去很少,其实信息量真的不小。下面分别来说明。 +
+ +# 二、网络的监听和接收 +
+先看一下循环监听,写得跟别人不一样,但是目的达到的是一样。 +
+ +``` c++ +void net_plugin_impl::start_listen_loop( ) { + auto socket = std::make_shared( std::ref( app().get_io_service() ) ); + //异步监听的lambada表达式 + acceptor->async_accept( *socket, [socket,this]( boost::system::error_code ec ) { + if( !ec ) { + uint32_t visitors = 0; + for (auto &conn : connections) { + if(conn->current() && conn->peer_addr.empty()) { + visitors++; + } + } + //判断新连接并增加计数 + if (num_clients != visitors) { + ilog ("checking max client, visitors = ${v} num clients ${n}",("v",visitors)("n",num_clients)); + num_clients = visitors; + } + if( max_client_count == 0 || num_clients < max_client_count ) { + ++num_clients; + connection_ptr c = std::make_shared( socket ); + connections.insert( c );//保存新连接的指针 + start_session( c ); + } else { + elog( "Error max_client_count ${m} exceeded", + ( "m", max_client_count) ); + socket->close( ); + } + start_listen_loop();//继续监听 + } else { + elog( "Error accepting connection: ${m}",( "m", ec.message() ) ); + } + }); +} +void net_plugin_impl::start_session( connection_ptr con ) { + boost::asio::ip::tcp::no_delay nodelay( true ); + con->socket->set_option( nodelay ); + start_read_message( con );//开始读取连接的消息 + ++started_sessions; + + // for now, we can just use the application main loop. + // con->readloop_complete = bf::async( [=](){ read_loop( con ); } ); + // con->writeloop_complete = bf::async( [=](){ write_loop con ); } ); +} +``` +其实上面的代码没什么特殊的,只是引用了BOOST的库,可能得熟悉一下,接着看如何读取消息,真正的数据交互在这里: +
+ +``` c++ +void net_plugin_impl::start_read_message( connection_ptr conn ) { + + try { + if(!conn->socket) { + return; + } + //真正的数据异步读取 + conn->socket->async_read_some + (conn->pending_message_buffer.get_buffer_sequence_for_boost_async_read(), + [this,conn]( boost::system::error_code ec, std::size_t bytes_transferred ) { + try { + if( !ec ) { + //判断是否超大小读取数据 + if (bytes_transferred > conn->pending_message_buffer.bytes_to_write()) { + elog("async_read_some callback: bytes_transfered = ${bt}, buffer.bytes_to_write = ${btw}", + ("bt",bytes_transferred)("btw",conn->pending_message_buffer.bytes_to_write())); + } + //判断是不是符合情况 + FC_ASSERT(bytes_transferred <= conn->pending_message_buffer.bytes_to_write()); + conn->pending_message_buffer.advance_write_ptr(bytes_transferred); + //处理数据 + while (conn->pending_message_buffer.bytes_to_read() > 0) { + uint32_t bytes_in_buffer = conn->pending_message_buffer.bytes_to_read(); + + if (bytes_in_buffer < message_header_size) { + break; + } else { + uint32_t message_length; + auto index = conn->pending_message_buffer.read_index(); + conn->pending_message_buffer.peek(&message_length, sizeof(message_length), index); + if(message_length > def_send_buffer_size*2) { + elog("incoming message length unexpected (${i})", ("i", message_length)); + close(conn); + return; + } + if (bytes_in_buffer >= message_length + message_header_size) { + conn->pending_message_buffer.advance_read_ptr(message_header_size); + if (!conn->process_next_message(*this, message_length)) { + return; + } + } else { + conn->pending_message_buffer.add_space(message_length + message_header_size - bytes_in_buffer); + break; + } + } + } + start_read_message(conn);//继续读取 + } else { + auto pname = conn->peer_name(); + if (ec.value() != boost::asio::error::eof) { + elog( "Error reading message from ${p}: ${m}",("p",pname)( "m", ec.message() ) ); + } else { + ilog( "Peer ${p} closed connection",("p",pname) ); + } + close( conn ); + } + } + catch(const std::exception &ex) { +...... + } +...... + } ); + } catch (...) { +...... + } +} + +/* + * 创建一个数据接收的缓冲区 + * Creates and returns a vector of boost mutable_buffers that can + * be passed to boost async_read() and async_read_some() functions. + * The beginning of the vector will be the write pointer, which + * should be advanced the number of bytes read after the read returns. + */ +std::vector get_buffer_sequence_for_boost_async_read() { + std::vector seq; + FC_ASSERT(write_ind.first < buffers.size()); + seq.push_back(boost::asio::buffer(&buffers[write_ind.first]->at(write_ind.second), + buffer_len - write_ind.second)); + for (std::size_t i = write_ind.first + 1; i < buffers.size(); i++) { + seq.push_back(boost::asio::buffer(&buffers[i]->at(0), buffer_len)); + } + return seq; +} +``` +
+ +# 三、网络的连接 +
+处理完成监听和接收,来看一下主动连接: +
+ +``` c++ +void net_plugin_impl::start_monitors() { + connector_check.reset(new boost::asio::steady_timer( app().get_io_service())); + transaction_check.reset(new boost::asio::steady_timer( app().get_io_service())); + start_conn_timer();//调用两个函数 + start_txn_timer(); +} +//调用的start_conn_timer +void net_plugin_impl::start_conn_timer( ) { + connector_check->expires_from_now( connector_period);// 设置定时器 + connector_check->async_wait( [&](boost::system::error_code ec) { + if( !ec) { + connection_monitor( );//调用连接监控 + } + else { + elog( "Error from connection check monitor: ${m}",( "m", ec.message())); + start_conn_timer( ); + } + }); +} +void net_plugin_impl::connection_monitor( ) { + start_conn_timer();//循环调用 + vector discards; + num_clients = 0; + for( auto &c : connections ) { + if( !c->socket->is_open() && !c->connecting) { + if( c->peer_addr.length() > 0) { + connect(c);//连接指定的点。 + } + else { + discards.push_back( c); + } + } else { + if( c->peer_addr.empty()) { + num_clients++; + } + } + } + //处理断开的连接 + if( discards.size( ) ) { + for( auto &c : discards) { + connections.erase( c ); + c.reset(); + } + } +} +//交易的定时器监视 +void net_plugin_impl::start_txn_timer() { + transaction_check->expires_from_now( txn_exp_period); + transaction_check->async_wait( [&](boost::system::error_code ec) { + if( !ec) { + expire_txns( );//处理到期交易的情况 + } + else { + elog( "Error from transaction check monitor: ${m}",( "m", ec.message())); + start_txn_timer( ); + } + }); +} +void net_plugin_impl::expire_txns() { + start_txn_timer( ); + auto &old = local_txns.get(); + auto ex_up = old.upper_bound( time_point::now()); + auto ex_lo = old.lower_bound( fc::time_point_sec( 0)); + old.erase( ex_lo, ex_up); + + auto &stale = local_txns.get(); + chain_controller &cc = chain_plug->chain(); + uint32_t bn = cc.last_irreversible_block_num(); + auto bn_up = stale.upper_bound(bn); + auto bn_lo = stale.lower_bound(1); + stale.erase( bn_lo, bn_up); +} +``` +
+最后看一看连接的代码: +
+ +``` c++ + +/** + * Used to trigger a new connection from RPC API + */ +string net_plugin::connect( const string& host ) { + if( my->find_connection( host ) ) + return "already connected"; + + connection_ptr c = std::make_shared(host); + fc_dlog(my->logger,"adding new connection to the list"); + my->connections.insert( c ); + fc_dlog(my->logger,"calling active connector"); + my->connect( c ); + return "added connection"; +} +//两个连接的重载,其实都很简单,第个Connect负责解析,第二个Connect负责真正连接 +void net_plugin_impl::connect( connection_ptr c ) { + if( c->no_retry != go_away_reason::no_reason) { + fc_dlog( logger, "Skipping connect due to go_away reason ${r}",("r", reason_str( c->no_retry ))); + return; + } + + auto colon = c->peer_addr.find(':'); + + if (colon == std::string::npos || colon == 0) { + elog ("Invalid peer address. must be \"host:port\": ${p}", ("p",c->peer_addr)); + return; + } + + auto host = c->peer_addr.substr( 0, colon ); + auto port = c->peer_addr.substr( colon + 1); + idump((host)(port)); + tcp::resolver::query query( tcp::v4(), host.c_str(), port.c_str() ); + // Note: need to add support for IPv6 too + + resolver->async_resolve( query, + [c, this]( const boost::system::error_code& err, + tcp::resolver::iterator endpoint_itr ){ + if( !err ) { + connect( c, endpoint_itr ); + } else { + elog( "Unable to resolve ${peer_addr}: ${error}", + ( "peer_addr", c->peer_name() )("error", err.message() ) ); + } + }); +} + +void net_plugin_impl::connect( connection_ptr c, tcp::resolver::iterator endpoint_itr ) { + if( c->no_retry != go_away_reason::no_reason) { + string rsn = reason_str(c->no_retry); + return; + } + auto current_endpoint = \*endpoint_itr; + ++endpoint_itr; + c->connecting = true; + c->socket->async_connect( current_endpoint, [c, endpoint_itr, this] ( const boost::system::error_code& err ) { + if( !err ) { + start_session( c );//读取数据 + c->send_handshake ();//发送握手 + } else { + if( endpoint_itr != tcp::resolver::iterator() ) { + c->close(); + connect( c, endpoint_itr ); + } + else { + elog( "connection failed to ${peer}: ${error}", + ( "peer", c->peer_name())("error",err.message())); + c->connecting = false; + my_impl->close(c); + } + } + } ); +} +``` +
+ +# 四、网络的数据同步 +
+在前面看了start_read_message,对内部没有怎么做细节的分析,网络也启动了,节点也发现了,那么P2P的职责开始实现了,首先就是同步数据,和比特币类似,也有一个中心的消息处理系统,名字都有点像。 +
+ +``` c++ +bool connection::process_next_message(net_plugin_impl& impl, uint32_t message_length) { + try { + // If it is a signed_block, then save the raw message for the cache + // This must be done before we unpack the message. + // This code is copied from fc::io::unpack(..., unsigned_int) + auto index = pending_message_buffer.read_index(); + uint64_t which = 0; char b = 0; uint8_t by = 0; + do { + pending_message_buffer.peek(&b, 1, index); + which |= uint32_t(uint8_t(b) & 0x7f) << by; + by += 7; + } while( uint8_t(b) & 0x80 ); + + if (which == uint64_t(net_message::tag::value)) { + blk_buffer.resize(message_length); + auto index = pending_message_buffer.read_index(); + pending_message_buffer.peek(blk_buffer.data(), message_length, index); + } + auto ds = pending_message_buffer.create_datastream(); + net_message msg; + fc::raw::unpack(ds, msg); + msgHandler m(impl, shared_from_this() );//impl是net_plugin_impl + msg.visit(m);//注意这里最终是调用一个仿函数,static_variant.hpp中 + } catch( const fc::exception& e ) { + edump((e.to_detail_string() )); + impl.close( shared_from_this() ); + return false; + } + return true; +} + +//仿函数实现是通过重载了小括号 +struct msgHandler : public fc::visitor { + net_plugin_impl &impl; + connection_ptr c; + msgHandler( net_plugin_impl &imp, connection_ptr conn) : impl(imp), c(conn) {} + + template + void operator()(const T &msg) const + { + impl.handle_message( c, msg); //这里会调用net_plugin_impl中的handle_message + } +}; +``` +
+下面开始调用分发函数: + +
+ +``` c++ +void net_plugin_impl::handle_message( connection_ptr c, const handshake_message &msg) { + fc_dlog( logger, "got a handshake_message from ${p} ${h}", ("p",c->peer_addr)("h",msg.p2p_address)); + if (!is_valid(msg)) { + elog( "Invalid handshake message received from ${p} ${h}", ("p",c->peer_addr)("h",msg.p2p_address)); + c->enqueue( go_away_message( fatal_other )); + return; + } + chain_controller& cc = chain_plug->chain(); + uint32_t lib_num = cc.last_irreversible_block_num( ); + uint32_t peer_lib = msg.last_irreversible_block_num; + if( c->connecting ) { + c->connecting = false; + } + if (msg.generation == 1) { + if( msg.node_id == node_id) { + elog( "Self connection detected. Closing connection"); + c->enqueue( go_away_message( self ) ); + return; + } + + if( c->peer_addr.empty() || c->last_handshake_recv.node_id == fc::sha256()) { + fc_dlog(logger, "checking for duplicate" ); + for(const auto &check : connections) { + if(check == c) + continue; + if(check->connected() && check->peer_name() == msg.p2p_address) { + // It's possible that both peers could arrive here at relatively the same time, so + // we need to avoid the case where they would both tell a different connection to go away. + // Using the sum of the initial handshake times of the two connections, we will + // arbitrarily (but consistently between the two peers) keep one of them. + if (msg.time + c->last_handshake_sent.time <= check->last_handshake_sent.time + check->last_handshake_recv.time) + continue; + + fc_dlog( logger, "sending go_away duplicate to ${ep}", ("ep",msg.p2p_address) ); + go_away_message gam(duplicate); + gam.node_id = node_id; + c->enqueue(gam); + c->no_retry = duplicate; + return; + } + } + } + else { + fc_dlog(logger, "skipping duplicate check, addr == ${pa}, id = ${ni}",("pa",c->peer_addr)("ni",c->last_handshake_recv.node_id)); + } + + if( msg.chain_id != chain_id) { + elog( "Peer on a different chain. Closing connection"); + c->enqueue( go_away_message(go_away_reason::wrong_chain) ); + return; + } + if( msg.network_version != network_version) { + if (network_version_match) { + elog("Peer network version does not match expected ${nv} but got ${mnv}", + ("nv", network_version)("mnv", msg.network_version)); + c->enqueue(go_away_message(wrong_version)); + return; + } else { + wlog("Peer network version does not match expected ${nv} but got ${mnv}", + ("nv", network_version)("mnv", msg.network_version)); + } + } + + if( c->node_id != msg.node_id) { + c->node_id = msg.node_id; + } + + if(!authenticate_peer(msg)) { + elog("Peer not authenticated. Closing connection."); + c->enqueue(go_away_message(authentication)); + return; + } + + bool on_fork = false; + fc_dlog(logger, "lib_num = ${ln} peer_lib = ${pl}",("ln",lib_num)("pl",peer_lib)); + + if( peer_lib <= lib_num && peer_lib > 0) { + try { + block_id_type peer_lib_id = cc.get_block_id_for_num( peer_lib); + on_fork =( msg.last_irreversible_block_id != peer_lib_id); + } + catch( const unknown_block_exception &ex) { + wlog( "peer last irreversible block ${pl} is unknown", ("pl", peer_lib)); + on_fork = true; + } + catch( ...) { + wlog( "caught an exception getting block id for ${pl}",("pl",peer_lib)); + on_fork = true; + } + if( on_fork) { + elog( "Peer chain is forked"); + c->enqueue( go_away_message( forked )); + return; + } + } + + if (c->sent_handshake_count == 0) { + c->send_handshake(); + } + } + + c->last_handshake_recv = msg; + sync_master->recv_handshake(c,msg);//这里开始同步 +} +void sync_manager::recv_handshake (connection_ptr c, const handshake_message &msg) { + chain_controller& cc = chain_plug->chain(); +...... + //-------------------------------- + // sync need checkz; (lib == last irreversible block) + // + // 0. my head block id == peer head id means we are all caugnt up block wise + // 1. my head block num < peer lib - start sync locally + // 2. my lib > peer head num - send an last_irr_catch_up notice if not the first generation + // + // 3 my head block num <= peer head block num - update sync state and send a catchup request + // 4 my head block num > peer block num ssend a notice catchup if this is not the first generation + // + //----------------------------- + + uint32_t head = cc.head_block_num( ); + block_id_type head_id = cc.head_block_id(); + if (head_id == msg.head_id) { + fc_dlog(logger, "sync check state 0"); + // notify peer of our pending transactions + notice_message note; + note.known_blocks.mode = none; + note.known_trx.mode = catch_up; + note.known_trx.pending = my_impl->local_txns.size(); + c->enqueue( note ); + return; + } + if (head < peer_lib) { + fc_dlog(logger, "sync check state 1"); + start_sync( c, peer_lib);//同步 + return; + } +...... +} + + + +``` +
+再深入的细节就不再分析了,就是基本的数据交互通信。 diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\350\231\232\346\213\237\346\234\272.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 0000000..f367a6f --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,686 @@ +# eos源码分析之五虚拟机 + +因为6月2日,blockone团队发布了上线的源码,所以从这里开始基于最新的1.01版本来分析。
+ +## 一、虚拟机的模块 + +虚拟机的代码主要分散在了以下几个目录, 主要在智能合约目录contracts,一些辅助的ABI的源码,区块链目录library/chain,是一些编译的接口,library/wasm-jit目录下,是主要的文件部分,然后在externals/src下也有相当一部分的二进制编译代码。其它一些目录下也有相关的一些文件,但比较分散代码也很少。重点分析编译过程。 +
+虚拟机的模块分成两部分,也就是编译部分和执行部分。智能合约在编译过程中会产生两个文件,一个是.wast,一个是.abi文件。 + +## 二、编译过程 + +### 1、wast文件的生成 +
+eoscpp是编译智能合约的命令,在tools目录下,eosiocpp.in中: + +``` c++ +function build_contract { + set -e + workdir=`mktemp -d` + + if [[ ${VERBOSE} == "1" ]]; then + PRINT_CMDS="set -x" + fi + + ($PRINT_CMDS; mkdir $workdir/built) + + for file in $@; do + name=`basename $file` + filePath=`dirname $file` + + ($PRINT_CMDS; @WASM_CLANG@ -emit-llvm -O3 --std=c++14 --target=wasm32 -nostdinc \ + -nostdlib -nostdlibinc -ffreestanding -nostdlib -fno-threadsafe-statics -fno-rtti \ + -fno-exceptions -I ${EOSIO_INSTALL_DIR}/include \ + -I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \ + -I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \ + -I${BOOST_INCLUDE_DIR} \ + -I $filePath \ + -c $file -o $workdir/built/$name + ) + + done + + ($PRINT_CMDS; @WASM_LLVM_LINK@ -only-needed -o $workdir/linked.bc $workdir/built/* \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/eosiolib.bc \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc++.bc \ + ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc.bc + + + ) + ($PRINT_CMDS; @WASM_LLC@ -thread-model=single --asm-verbose=false -o $workdir/assembly.s $workdir/linked.bc) + ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-s2wasm -o $outname -s 16384 $workdir/assembly.s) + ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-wast2wasm $outname ${outname%.\*}.wasm -n) + + ($PRINT_CMDS; rm -rf $workdir) + set +e +} +``` +
+首先调用了 @WASM_CLANG@ -emit-llvm -O3的编译,这和安装LLVM和CLANG有必然的关系。然后它会调用相关的链接库,关键还是最后几行代码: +
bin/eosio-s2wasm和bin/eosio-wast2wasm。 +
+从这里基本已经看出LLVM还是要和EOS内部的一些代码一起工作,才能搞定所有的流程。主要的编译工作由LLVM及其相关的模块构成,在这个过程中使用了一种叫做C++ without Emscripten的过程即:直接用 clang 的前端编译到 LLVM 的 bc,然后 llc 编译到汇编文件 s,再用 Binaryen 的工具 s2wasm 从汇编文件编译到 wasm 的 ast 文件 wast,最后用 wasm-as 编译到 wasm。 +
可能为了数据的通用性和更好的适配性,编译过程中的许多文件都提供了相关工具命令可以来回转换,比如a.ll和a.bc之间可以通过llvm-as和llvm-dis命令相互转换。 +
+LLVM IR主要有三种格式:一种是在内存中的编译中间语言;一种是硬盘上存储的二进制中间语言(以.bc结尾),最后一种是可读的中间格式(以.ll结尾)。这三种中间格式是完全相等的。 +
主要编译的流程基本如下面这样: +
+cpp-(CLANG+LLVM工具)-> \*.bc-(LLVM)->\*.s-(Binaryen)->s2wasm-(Binaryen)->wasm2wast--->\*.wast +
+
+abi文件在WIKI中可以找到,但是在WIKI中没有wast的相关格式,下面的wast文件的内容是从EMCC的官网上扒下来的: +
+ +``` c++ +;; tests/hello_world.c:4 +(drop + (call $\_printf + (i32.const 1144) + (get_local $$vararg_buffer) + ) +) +;; tests/hello_world.c:5 +(return + (i32.const 0) +) +``` +
+明白了编译流程再看源码就清楚很多了,为了保证多种数据的加载,就得写一些相关的加载的代码,举一个例子: +
+ +``` c++ +class wasm_runtime_interface { +...... +}; +class binaryen_runtime : public eosio::chain::wasm_runtime_interface +{......}; +class wavm_runtime : public eosio::chain::wasm_runtime_interface + {.....}; +``` +
+也就是说,要保证前面说过的相关文件的正确加载,特别是好多可以互相转换的文件的加载。下面以编译一个Assembly(*.wast--->*.wasm)为例分析一下: libraries/wasm-jit/Source/Programs中的Assemble.cpp +
+ +``` c++ +int commandMain(int argc,char** argv) +{ +...... + + // Load the WAST module. + IR::Module module; + if(!loadTextModule(inputFilename,module)) { return EXIT_FAILURE; } + +...... + + // Write the binary module. + if(!saveBinaryModule(outputFilename,module)) { return EXIT_FAILURE; } + + return EXIT_SUCCESS; +} + +``` +
+工作其实非常简单,加载WAST的模块到中间IR,然后保存成二进制的文件。保存的那个函数非常简单没啥可说的,分析下加载: +
+ +``` c++ +inline bool loadTextModule(const char* filename,IR::Module& outModule) +{ + // Read the file into a string. + auto wastBytes = loadFile(filename); +..... + + return loadTextModule(filename,wastString,outModule); +} +inline bool loadTextModule(const char* filename,const std::string& wastString,IR::Module& outModule) +{ + std::vector parseErrors; + //分析WASM中的模块,在webassembly中,实例都是以模块出现的,详情可看LLVM及webassembly + WAST::parseModule(wastString.c_str(),wastString.size(),outModule,parseErrors); + if(!parseErrors.size()) { return true; } + else + { +...... + } +} +bool parseModule(const char* string,Uptr stringLength,IR::Module& outModule,std::vector& outErrors) +{ + Timing::Timer timer; + + // Lex the string. + LineInfo* lineInfo = nullptr; + std::vector unresolvedErrors; + Token* tokens = lex(string,stringLength,lineInfo); + ModuleParseState state(string,lineInfo,unresolvedErrors,tokens,outModule); + + try + { + // Parse (module ...) + parseParenthesized(state,[&] + { + require(state,t_module); + parseModuleBody(state); + }); + require(state,t_eof); + } +...... +} +} +void parseModuleBody(ModuleParseState& state) +{ + const Token* firstToken = state.nextToken; + + // Parse the module's declarations. + while(state.nextToken->type != t_rightParenthesis) + { + parseDeclaration(state);//直接调用声明分析,用来判断跳转到哪个部分进行具体的分析 + }; + +...... + IR::setDisassemblyNames(state.module,state.disassemblyNames); +} +static void parseDeclaration(ModuleParseState& state) +{ + parseParenthesized(state,[&] + { + switch(state.nextToken->type) + { + //WebAssembly 中的导入的相关符号,并进入相关的分析函数 + case t_import: parseImport(state); return true; + case t_export: parseExport(state); return true; + case t_global: parseGlobal(state); return true; + case t_memory: parseMemory(state); return true; + case t_table: parseTable(state); return true; + case t_type: parseType(state); return true; + case t_data: parseData(state); return true; + case t_elem: parseElem(state); return true; + case t_func: parseFunc(state); return true; + case t_start: parseStart(state); return true; + default: + parseErrorf(state,state.nextToken,"unrecognized definition in module"); + throw RecoverParseException(); + }; + }); +} +//只列举其中一个Start +static void parseStart(ModuleParseState& state) +{ + require(state,t_start); + + Reference functionRef; + if(!tryParseNameOrIndexRef(state,functionRef)) + { + parseErrorf(state,state.nextToken,"expected function name or index"); + } + + state.postDeclarationCallbacks.push_back([functionRef](ModuleParseState& state) + { + state.module.startFunctionIndex = resolveRef(state,state.functionNameToIndexMap,state.module.functions.size(),functionRef); + }); +} +//最后写IR +void setDisassemblyNames(Module& module,const DisassemblyNames& names) +{ + // Replace an existing name section if one is present, or create a new section. + Uptr userSectionIndex = 0; + if(!findUserSection(module,"name",userSectionIndex)) + { + userSectionIndex = module.userSections.size(); + module.userSections.push_back({"name",{}}); + } + + ArrayOutputStream stream; + + Uptr numFunctionNames = names.functions.size(); + serializeVarUInt32(stream,numFunctionNames); + + for(Uptr functionIndex = 0;functionIndex < names.functions.size();++functionIndex) + { + std::string functionName = names.functions[functionIndex].name; + serialize(stream,functionName); + + Uptr numLocalNames = names.functions[functionIndex].locals.size(); + serializeVarUInt32(stream,numLocalNames); + for(Uptr localIndex = 0;localIndex < numLocalNames;++localIndex) + { + std::string localName = names.functions[functionIndex].locals[localIndex]; + serialize(stream,localName); + } + } + + module.userSections[userSectionIndex].data = stream.getBytes(); +} +``` +
+这里分析的比较浅,并没有深入到内部去分析,其实到内部后就是真正的词法主义啥的分析了,有兴趣可以去LLVM的官网或者EMCC的官网去看相关的资料。 +
+ +### 2、abi文件的生成 + +
+abi文件是一个JSON文件,主要是解释如何将用户动作在JSON和二进制表达之间转换。ABI还解释了如何将数据库状态转换为JSON或从JSON转换数据库状态。通过ABI描述了智能合约,开发人员和用户就可以通过JSON无缝地与相关的合约进行交互。下面是从EOS的WIKI上找的ABI的文件: +
+ +``` c++ +{ + "____comment": "This file was generated by eosio-abigen. DO NOT EDIT - 2018-05-07T21:16:48", + "types": [], + "structs": [{ + "name": "hi", + "base": "", + "fields": [{ + "name": "user", + "type": "account_name" + } + ] + } + ], + "actions": [{ + "name": "hi", + "type": "hi", + "ricardian_contract": "" + } + ], + "tables": [], + "ricardian_clauses": [] +} +``` + +
+在eosiocpp.in中可以看到下面的代码: +
+ +``` c++ +function generate_abi { + + if [[ ! -e "$1" ]]; then + echo "You must specify a file" + exit 1 + fi + + context_folder=$(cd "$(dirname "$1")" ; pwd -P) + + ${ABIGEN} -extra-arg=-c -extra-arg=--std=c++14 -extra-arg=--target=wasm32 \ + -extra-arg=-nostdinc -extra-arg=-nostdinc++ -extra-arg=-DABIGEN \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \ + -extra-arg=-I${BOOST_INCLUDE_DIR} \ + -extra-arg=-I${EOSIO_INSTALL_DIR}/include -extra-arg=-I$context_folder \ + -extra-arg=-fparse-all-comments -destination-file=${outname} -verbose=0 \ + -context=$context_folder $1 -- + + if [ "$?" -ne 0 ]; then + exit 1 + fi + + echo "Generated ${outname} ..." +} +``` +
+abi文件的生成的main程序在programs/eosio-abigen下,主要内容如下: +
+ +``` c++ +using mvo = fc::mutable_variant_object; +//FrontendActionFactory 是用来产生FrontendAction的一个抽象接口,而FrontendAction又是一个Clang中的抽象的前台动作基类 +std::unique_ptr create_factory(bool verbose, bool opt_sfs, string abi_context, abi_def& output, const string& contract, const vector& actions) { + + struct abi_frontend_action_factory : public FrontendActionFactory { + + bool verbose; + bool opt_sfs; + string abi_context; + abi_def& output; + const string& contract; + const vector& actions; + + abi_frontend_action_factory(bool verbose, bool opt_sfs, string abi_context, + abi_def& output, const string& contract, const vector& actions) : verbose(verbose), + abi_context(abi_context), output(output), contract(contract), actions(actions) {} + + clang::FrontendAction \*create() override { + //创建一个generate_abi_action对象,这个对象是生成ABI的重要部分 + return new generate_abi_action(verbose, opt_sfs, abi_context, output, contract, actions); + } + + }; + + return std::unique_ptr( + new abi_frontend_action_factory(verbose, opt_sfs, abi_context, output, contract, actions) + ); +} +//这个函数用来处理接口宏 +std::unique_ptr create_find_macro_factory(string& contract, vector& actions, string abi_context) { + + struct abi_frontend_macro_action_factory : public FrontendActionFactory { + + string& contract; + vector& actions; + string abi_context; + + abi_frontend_macro_action_factory (string& contract, vector& actions, + string abi_context ) : contract(contract), actions(actions), abi_context(abi_context) {} + + clang::FrontendAction \*create() override { + return new find_eosio_abi_macro_action(contract, actions, abi_context); + } + + }; + + return std::unique_ptr( + new abi_frontend_macro_action_factory(contract, actions, abi_context) + ); +} +//LLVM选项处理类 +static cl::OptionCategory abi_generator_category("ABI generator options"); + + //扩展命令行选项,类似于增加了对选项的各种操作,如连接等 +static cl::opt abi_context( + "context", + cl::desc("ABI context"), + cl::cat(abi_generator_category)); + +static cl::opt abi_destination( + "destination-file", + cl::desc("destination json file"), + cl::cat(abi_generator_category)); + +static cl::opt abi_verbose( + "verbose", + cl::desc("show debug info"), + cl::cat(abi_generator_category)); + +static cl::opt abi_opt_sfs( + "optimize-sfs", + cl::desc("Optimize single field struct"), + cl::cat(abi_generator_category)); + +int main(int argc, const char **argv) { abi_def output; try { + CommonOptionsParser op(argc, argv, abi_generator_category); + ClangTool Tool(op.getCompilations(), op.getSourcePathList()); + + string contract; + vector actions; + int result = Tool.run(create_find_macro_factory(contract, actions, abi_context).get()); + if(!result) { + result = Tool.run(create_factory(abi_verbose, abi_opt_sfs, abi_context, output, contract, actions).get()); + if(!result) { + abi_serializer(output).validate(); + fc::variant vabi; + to_variant(output, vabi); + + auto comment = fc::format_string( + "This file was generated by eosio-abigen. DO NOT EDIT - ${ts}", + mvo("ts",fc::time_point_sec(fc::time_point::now()).to_iso_string())); + + //处理一声明内容,看一下ABI的格式就明白了 + auto abi_with_comment = mvo("____comment", comment)(mvo(vabi)); + fc::json::save_to_file(abi_with_comment, abi_destination, true); + } + } + return result; +} FC_CAPTURE_AND_LOG((output)); return -1; } + +``` +
+从上面的Main函数可以看,先要查找相关的ABI宏,再根据这个宏,用工厂类创建ABI的创建对象。当然,在前面要使用CLANG的一些分析工具对象。find_eosio_abi_macro_action这个宏主要是对整个智能合约的宏进行解析: +
+ +``` c++ +struct find_eosio_abi_macro_action : public PreprocessOnlyAction { + + string& contract; + vector& actions; + const string& abi_context; + + find_eosio_abi_macro_action(string& contract, vector& actions, const string& abi_context + ): contract(contract), + actions(actions), abi_context(abi_context) { + } + + struct callback_handler : public PPCallbacks { + + CompilerInstance& compiler_instance; + find_eosio_abi_macro_action& act; + + callback_handler(CompilerInstance& compiler_instance, find_eosio_abi_macro_action& act) + : compiler_instance(compiler_instance), act(act) {} + + void MacroExpands (const Token &token, const MacroDefinition &md, SourceRange range, const MacroArgs *args) override { + + auto* id = token.getIdentifierInfo(); + if( id == nullptr ) return; + if( id->getName() != "EOSIO_ABI" ) return;//看到这个宏没有,这是智能合约里动态创建的标志 + + const auto& sm = compiler_instance.getSourceManager(); + auto file_name = sm.getFilename(range.getBegin()); + if ( !act.abi_context.empty() && !file_name.startswith(act.abi_context) ) { + return; + } + + ABI_ASSERT( md.getMacroInfo()->getNumArgs() == 2 ); + + clang::SourceLocation b(range.getBegin()), _e(range.getEnd()); + clang::SourceLocation e(clang::Lexer::getLocForEndOfToken(\_e, 0, sm, compiler_instance.getLangOpts())); + auto macrostr = string(sm.getCharacterData(b), sm.getCharacterData(e)-sm.getCharacterData(b)); + + //正则匹配,编译器的标配 + //regex r(R"(EOSIO_ABI\s*\(\s*(.+?)\s*,((?:.+?)*)\s*\))");//注释掉是因为格式的问题 fjf 6.7 + smatch smatch; + auto res = regex_search(macrostr, smatch, r); + ABI_ASSERT( res ); + + act.contract = smatch[1].str(); + + auto actions_str = smatch[2].str(); + boost::trim(actions_str); + actions_str = actions_str.substr(1); + actions_str.pop_back(); + boost::remove_erase_if(actions_str, boost::is_any_of(" (")); + + boost::split(act.actions, actions_str, boost::is_any_of(")")); + } + }; + + void ExecuteAction() override { + getCompilerInstance().getPreprocessor().addPPCallbacks( + llvm::make_unique(getCompilerInstance(), *this) + ); + PreprocessOnlyAction::ExecuteAction(); + }; + +}; + +``` +
+这些个完成后,在Main函数中进行abi_serializer,最后保存到文件,ABI就这个产生了。当然,这背后的细节LLVM和CLAN做了好多,感兴趣的可以多在其官网上看看,最近看虚拟机和JAVA的对比,再和c++编译器编译对比,收益还是颇大。 +
+最后看一下这个类: class generate_abi_action : public ASTFrontendAction{......},这个类在前边的工厂里进行了创建,但是其中有一个主要的函数 +
+ +``` c++ +std::unique_ptr CreateASTConsumer(CompilerInstance& compiler_instance, + llvm::StringRef) override { + return llvm::make_unique(compiler_instance, abi_gen); +} +``` +
+这个函数是内部调用的,因为,它是protected的类型。在Compile之前,创建ASTConsumer。在建立AST(抽象语法树)的过程中,ASTConsumer提供了众多的Hooks。被FrontendAction的公共接口BeginSourceFile调用。 +
+这里最终会调用abi_generator对象,其中void abi_generator::handle_decl(const Decl* decl)这个函数,用来处理具体的细节。 +
+
+
+
+
+
+ + +## 三、执行过程 + +
+加载到虚拟机的过程其实就是JIT做的事儿了,有兴趣可以分析一下wast-jit这个文件下的部分,特别是Runtime内部的一些代码,这里主要分析一下加载过程,在programs/cleos中的主函数中: +
+ +``` c++ +int main(int argc,char**argv) +{ + ...... + // set subcommand + auto setSubcommand = app.add_subcommand("set", localized("Set or update blockchain state")); + setSubcommand->require_subcommand(); + + // set contract subcommand + string account; + string contractPath; + string wastPath; + string abiPath; + bool shouldSend = true; + auto codeSubcommand = setSubcommand->add_subcommand("code", localized("Create or update the code on an account")); + codeSubcommand->add_option("account", account, localized("The account to set code for"))->required(); + codeSubcommand->add_option("code-file", wastPath, localized("The fullpath containing the contract WAST or WASM"))->required(); + + auto abiSubcommand = setSubcommand->add_subcommand("abi", localized("Create or update the abi on an account")); + abiSubcommand->add_option("account", account, localized("The account to set the ABI for"))->required(); + abiSubcommand->add_option("abi-file", abiPath, localized("The fullpath containing the contract WAST or WASM"))->required(); + + auto contractSubcommand = setSubcommand->add_subcommand("contract", localized("Create or update the contract on an account")); + contractSubcommand->add_option("account", account, localized("The account to publish a contract for")) + ->required(); + contractSubcommand->add_option("contract-dir", contractPath, localized("The path containing the .wast and .abi")) + ->required(); + contractSubcommand->add_option("wast-file", wastPath, localized("The file containing the contract WAST or WASM relative to contract-dir")); + // ->check(CLI::ExistingFile); + auto abi = contractSubcommand->add_option("abi-file,-a,--abi", abiPath, localized("The ABI for the contract relative to contract-dir")); + // ->check(CLI::ExistingFile); + + //处理智能合约 + std::vector actions; + auto set_code_callback = [&]() { + std::string wast; + fc::path cpath(contractPath); + + if( cpath.filename().generic_string() == "." ) cpath = cpath.parent_path(); + + if( wastPath.empty() ) + { + wastPath = (cpath / (cpath.filename().generic_string()+".wasm")).generic_string(); + if (!fc::exists(wastPath)) + wastPath = (cpath / (cpath.filename().generic_string()+".wast")).generic_string(); + } + + std::cout << localized(("Reading WAST/WASM from " + wastPath + "...").c_str()) << std::endl; + fc::read_file_contents(wastPath, wast); + FC_ASSERT( !wast.empty(), "no wast file found ${f}", ("f", wastPath) ); + vector wasm; + const string binary_wasm_header("\x00\x61\x73\x6d", 4); + if(wast.compare(0, 4, binary_wasm_header) == 0) { + std::cout << localized("Using already assembled WASM...") << std::endl; + wasm = vector(wast.begin(), wast.end()); + } + else { + std::cout << localized("Assembling WASM...") << std::endl; + wasm = wast_to_wasm(wast);//处理可见文件与二进制的执行形式 + } + + actions.emplace_back( create_setcode(account, bytes(wasm.begin(), wasm.end()) ) ); + if ( shouldSend ) { + std::cout << localized("Setting Code...") << std::endl; + send_actions(std::move(actions), 10000, packed_transaction::zlib); + } + }; + + //处理ABI的加载 + auto set_abi_callback = [&]() { + fc::path cpath(contractPath); + if( cpath.filename().generic_string() == "." ) cpath = cpath.parent_path(); + + if( abiPath.empty() ) + { + abiPath = (cpath / (cpath.filename().generic_string()+".abi")).generic_string(); + } + + FC_ASSERT( fc::exists( abiPath ), "no abi file found ${f}", ("f", abiPath) ); + + try { + actions.emplace_back( create_setabi(account, fc::json::from_file(abiPath).as()) ); + } EOS_RETHROW_EXCEPTIONS(abi_type_exception, "Fail to parse ABI JSON") + if ( shouldSend ) { + std::cout << localized("Setting ABI...") << std::endl; + send_actions(std::move(actions), 10000, packed_transaction::zlib); + } + }; + + add_standard_transaction_options(contractSubcommand, "account@active"); + add_standard_transaction_options(codeSubcommand, "account@active"); + add_standard_transaction_options(abiSubcommand, "account@active"); + contractSubcommand->set_callback([&] { + shouldSend = false; + set_code_callback(); + set_abi_callback(); + std::cout << localized("Publishing contract...") << std::endl; + send_actions(std::move(actions), 10000, packed_transaction::zlib); + }); + codeSubcommand->set_callback(set_code_callback); + abiSubcommand->set_callback(set_abi_callback); + ...... +} +``` +
+这里只分析一下wast->wasm的转换: +
+ +``` c++ +std::vector wast_to_wasm( const std::string& wast ) +{ + std::stringstream ss; + + try { + IR::Module module; //中间语言 + std::vector parse_errors; + //这里用到了jit的对象 + WAST::parseModule(wast.c_str(),wast.size(),module,parse_errors);//以Module为单元分析文件中的数据 +...... + //按照LLVM的编译要求处理节 + for(auto sectionIt = module.userSections.begin();sectionIt != module.userSections.end();++sectionIt) + { + if(sectionIt->name == "name") { module.userSections.erase(sectionIt); break; } + } + + try + { + // Serialize the WebAssembly module.串行化,其实就是二进制化 + Serialization::ArrayOutputStream stream; + WASM::serialize(stream,module); + return stream.getBytes(); + } + catch(const Serialization::FatalSerializationException& exception) + { + ss << "Error serializing WebAssembly binary file:" << std::endl; + ss << exception.message << std::endl; + FC_ASSERT( !"error converting to wasm", "${msg}", ("msg",ss.get()) ); + } catch(const IR::ValidationException& e) { + ss << "Error validating WebAssembly binary file:" << std::endl; + ss << e.message << std::endl; + FC_ASSERT( !"error converting to wasm", "${msg}", ("msg",ss.get()) ); + } + +} FC_CAPTURE_AND_RETHROW( (wast) ) } /// wast_to_wasm +//其下两个是分别处理不同类型的文件来源 +std::string wasm_to_wast( const std::vector& wasm ) { + return wasm_to_wast( wasm.data(), wasm.size() ); +} /// wasm_to_wast + +std::string wasm_to_wast( const uint8_t* data, uint64_t size ) +{ try { + IR::Module module; + Serialization::MemoryInputStream stream((const U8*)data,size); + WASM::serialize(stream,module); + // Print the module to WAST. + return WAST::print(module); +} FC_CAPTURE_AND_RETHROW() } +``` +
+基本到现在就把虚拟机简要的分析了一下,其中有好多关于CLANG,LLVM和Webassembly的知识,需要在看这篇文章前了解一下。 +
diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253\345\214\272\345\235\227\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253\345\214\272\345\235\227\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 0000000..57f770e --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\253\345\214\272\345\235\227\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,600 @@ +# eos源码分析之八区块及数据结构 + +做为EOS系列的最后一篇,把区块及相关的数据结构分析一下。虽然在前面的共识中分析过出块这部分,但对EOS的区块结构及一些细节并没有深入进去。 +
+ +## 一、区块 +EOS的区块设计不同的版本变化很大,这里以4.0的为模板分析,先看一下它的数据结构: +
+ +``` c++ +struct block_header +{ + block_timestamp_type timestamp; + account_name producer;//帐户标识符 13字节 + + uint16_t confirmed = 1; + + block_id_type previous;//前一块的HASH + + checksum256_type transaction_mroot; /// mroot of cycles_summary + checksum256_type action_mroot; /// mroot of all delivered action receipts + + uint32_t schedule_version = 0; + optional new_producers;//新生产者 + extensions_type header_extensions; + + + digest_type digest()const;//摘要哈希 + block_id_type id() const; //自己的哈希 + uint32_t block_num() const { return num_from_id(previous) + 1; } + static uint32_t num_from_id(const block_id_type& id);//ID是任意数字,区块号是从零长到现在的排序号 ID=HASH+n +}; + +struct signed_block_header : public block_header +{ + signature_type producer_signature;//生产者签名 +}; +struct signed_block : public signed_block_header { + using signed_block_header::signed_block_header; + signed_block() = default; + signed_block( const signed_block_header& h ):signed_block_header(h){} + + vector transactions; /// new or generated transactions交易记录 + extensions_type block_extensions;//扩展区 +}; +using signed_block_ptr = std::shared_ptr;//重定义一个新的数据类型,方便使用 +``` +
+这里感觉最大的不同是把原来的交易ID直接弄成了交易内容,这样有点简单粗暴的感觉,但是确实是容易理解一些。区块的生产在前面选举后分析过,这里不再赘述,看一下产生区块中对交易的处理。 +
+ +## 二、交易和上链 +
+正如所有的区块链一样,交易最终打包入区块,才是真正的区块成功能,也就是说,区块生产出来的目的不是单纯生产块,而要把交易数据打包进去,然后再保存到数据库,最终上链。 +
+ +### 1、交易 +
+ +``` c++ +transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx, + fc::time_point deadline, + bool implicit, + uint32_t billed_cpu_time_us ) +{ + FC_ASSERT(deadline != fc::time_point(), "deadline cannot be uninitialized"); + + transaction_trace_ptr trace;//交易检索 + try { + transaction_context trx_context(self, trx->trx, trx->id); //交易管理控制 + trx_context.deadline = deadline; + trx_context.billed_cpu_time_us = billed_cpu_time_us; + trace = trx_context.trace; + try { + if( implicit ) { + trx_context.init_for_implicit_trx(); + } else { + trx_context.init_for_input_trx( trx->packed_trx.get_unprunable_size(), + trx->packed_trx.get_prunable_size(), + trx->trx.signatures.size() ); + } + + //检查权限集合 + if( !implicit && pending->_block_status == controller::block_status::incomplete ) { + check_actor_list( trx_context.bill_to_accounts ); // Assumes bill_to_accounts is the set of actors authorizing the transaction + } + + //延迟状态 + trx_context.delay = fc::seconds(trx->trx.delay_sec); + + //检查权限,这个前面分析过 + if( !self.skip_auth_check() && !implicit ) { + authorization.check_authorization( + trx->trx.actions, + trx->recover_keys( chain_id ), + {}, + trx_context.delay, + [](){} + /*std::bind(&transaction_context::add_cpu_usage_and_check_time, &trx_context, + std::placeholders::_1)*/, + false + ); + } + + //执行上下文,其实就是执行tx中的action + trx_context.exec(); + trx_context.finalize(); // Automatically rounds up network and CPU usage in trace and bills payers if successful + + //创建恢复点 + auto restore = make_block_restore_point(); + + if (!implicit) { + transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0)) + ? transaction_receipt::executed + : transaction_receipt::delayed; + //交易填充 + trace->receipt = push_receipt(trx->packed_trx, s, trx_context.billed_cpu_time_us, trace->net_usage); + pending->_pending_block_state->trxs.emplace_back(trx); + } else { + transaction_receipt_header r; + r.status = transaction_receipt::executed; + r.cpu_usage_us = trx_context.billed_cpu_time_us; + r.net_usage_words = trace->net_usage / 8; + trace->receipt = r; + } + //填充ACTION + fc::move_append(pending->_actions, move(trx_context.executed)); + + // call the accept signal but only once for this transaction + if (!trx->accepted) { + emit( self.accepted_transaction, trx); + trx->accepted = true; + } + + emit(self.applied_transaction, trace);// 发送成功打包交易的消息 + + trx_context.squash();//不敢肯定,是不是清理回退的数据 + restore.cancel();//取消恢复 + + if (!implicit) { + unapplied_transactions.erase( trx->signed_id ); + } + return trace; + } catch (const fc::exception& e) { + trace->except = e; + trace->except_ptr = std::current_exception(); + } + + if (!failure_is_subjective(*trace->except)) { + unapplied_transactions.erase( trx->signed_id ); + } + + return trace; + } FC_CAPTURE_AND_RETHROW((trace)) +} +``` +
+ +### 2、上链 +
+在apply_block中: +
+ +``` c++ +void commit_block( bool add_to_fork_db ) { + if( add_to_fork_db ) { + pending->_pending_block_state->validated = true; + auto new_bsp = fork_db.add( pending->_pending_block_state ); + emit( self.accepted_block_header, pending->_pending_block_state ); + head = fork_db.head(); + FC_ASSERT( new_bsp == head, "committed block did not become the new head in fork database" ); + + } + +// ilog((fc::json::to_pretty_string(*pending->_pending_block_state->block))); + emit( self.accepted_block, pending->_pending_block_state ); + + if( !replaying ) { + reversible_blocks.create( [&]( auto& ubo ) { + ubo.blocknum = pending->_pending_block_state->block_num; + ubo.set_block( pending->_pending_block_state->block ); + }); + } + + pending->push(); + pending.reset();//恢复状态,可以再次出块 + +} +``` +
+通过fork_db的操作把数据库存储起来,然后挂到链上,形成区块链。再广播出去,清除状态,重新准备出块。 +
+
+ +## 三、相关的几个数据结构 + +
+有几个数据结构比较重要:multi_index,optional和scoped_exit。 +
+
+ +### 1、访问数据库的multi_index +
+ +``` c++ +template +class multi_index +{ + private: + + static_assert( sizeof...(Indices) <= 16, "multi_index only supports a maximum of 16 secondary indices" ); + + constexpr static bool validate_table_name( uint64_t n ) { + // Limit table names to 12 characters so that the last character (4 bits) can be used to distinguish between the secondary indices. + return (n & 0x000000000000000FULL) == 0; + } + + constexpr static size_t max_stack_buffer_size = 512; + + static_assert( validate_table_name(TableName), "multi_index does not support table names with a length greater than 12"); + + uint64_t _code; + uint64_t _scope; + + mutable uint64_t _next_primary_key; + + enum next_primary_key_tags : uint64_t { + no_available_primary_key = static_cast(-2), // Must be the smallest uint64_t value compared to all other tags + unset_next_primary_key = static_cast(-1) + }; + + struct item : public T + { + template + item( const multi_index* idx, Constructor&& c ) + :\__idx(idx){ + c(\*this); + } +...... + }; + + struct item_ptr + { + item_ptr(std::unique_ptr&& i, uint64_t pk, int32_t pitr) + : \_item(std::move(i)), \_primary_key(pk), \_primary_itr(pitr) {} + +...... + }; + + mutable std::vector _items_vector; + + template + struct index { + public: + typedef Extractor secondary_extractor_type; + typedef typename std::decay::type secondary_key_type; +...... + + constexpr static uint64_t name() { return index_table_name; } + constexpr static uint64_t number() { return Number; } + + struct const_iterator : public std::iterator { + public: + friend bool operator == ( const const_iterator& a, const const_iterator& b ) { + return a.\_item == b.\_item; + } + friend bool operator != ( const const_iterator& a, const const_iterator& b ) { + return a.\_item != b.\_item; + } + + const T& operator*()const { return *static_cast(\_item); } + const T* operator->()const { return static_cast(\_item); } + +...... + + return *this; + } + + const_iterator& operator--() { + using namespace \_multi_index_detail; + +...... + + return \*this; + } + + const_iterator():_item(nullptr){} + private: + friend struct index; + const_iterator( const index* idx, const item* i = nullptr ) + : _idx(idx), _item(i) {} + + const index* _idx; + const item* _item; + }; /// struct multi_index::index::const_iterator + + typedef std::reverse_iterator const_reverse_iterator; + + const_iterator cbegin()const { + using namespace \_multi_index_detail; + return lower_bound( secondary_key_traits::lowest() ); + } +...... + + const T& get( secondary_key_type&& secondary, const char* error_msg = "unable to find secondary key" )const { + return get( secondary, error_msg ); + } + + // Gets the object with the smallest primary key in the case where the secondary key is not unique. + const T& get( const secondary_key_type& secondary, const char* error_msg = "unable to find secondary key" )const { + auto result = find( secondary ); + eosio_assert( result != cend(), error_msg ); + return *result; + } + + const_iterator lower_bound( secondary_key_type&& secondary )const { + return lower_bound( secondary ); + } + const_iterator lower_bound( const secondary_key_type& secondary )const { + using namespace \_multi_index_detail; +...... + + return {this, &mi}; + } + + const_iterator upper_bound( secondary_key_type&& secondary )const { + return upper_bound( secondary ); + } + const_iterator upper_bound( const secondary_key_type& secondary )const { +...... + + return {this, &mi}; + } + + const_iterator iterator_to( const T& obj ) { +...... + return {this, &objitem}; + } +...... + + static auto extract_secondary_key(const T& obj) { return secondary_extractor_type()(obj); } + + private: + friend class multi_index; + + index( typename std::conditional::type midx ) + :_multidx(midx){} + + typename std::conditional::type _multidx; + }; /// struct multi_index::index + +...... + const item* ptr = itm.get(); + auto pk = itm->primary_key(); + auto pitr = itm->__primary_itr; + + _items_vector.emplace_back( std::move(itm), pk, pitr ); + + return *ptr; + } /// load_object_by_primary_iterator + + public: + + multi_index( uint64_t code, uint64_t scope ) + :_code(code),_scope(scope),_next_primary_key(unset_next_primary_key) + {} + +...... + + _item = &_multidx->load_object_by_primary_iterator( prev_itr ); + return *this; + } + + private: + const_iterator( const multi_index* mi, const item* i = nullptr ) + :_multidx(mi),_item(i){} + + const multi_index* _multidx; + const item* _item; + friend class multi_index; + }; /// struct multi_index::const_iterator + + typedef std::reverse_iterator const_reverse_iterator; + + const_iterator cbegin()const { + return lower_bound(std::numeric_limits::lowest()); + } + const_iterator begin()const { return cbegin(); } + +....... + + void erase( const T& obj ) { + using namespace \_multi_index_detail; + +...... + + hana::for_each( \_indices, [&]( auto& idx ) { + typedef typename decltype(+hana::at_c<0>(idx))::type index_type; + + auto i = objitem.__iters[index_type::number()]; + if( i < 0 ) { + typename index_type::secondary_key_type secondary; + i = secondary_index_db_functions::db_idx_find_primary( \_code, \_scope, index_type::name(), objitem.primary_key(), secondary ); + } + if( i >= 0 ) + secondary_index_db_functions::db_idx_remove( i ); + }); + } + +}; + +``` +
+multi_index这个数据结构同样是仿照BOOST库中的boost::multi_index;估计EOS的开发人员觉得这个太重,自己搞了一个,当然,顺带实现很多自己独立的需求。需要说明的是WIKI上的说明是比较落后的,而且EOS开发团队也声明了,这个容器对象是不断演进的,所以说现在分析的可能已经是落后的了,但可能他们的大原则不会有剧烈的变动。 +
+EOS为每个账户都预留了数据库空间(大小与代币持有量有关),账户可以建立多个数据表。智能合约无法直接操作存储在见证人硬盘中的数据表,需要使用multi_index作为中间工具(或者叫容器),每个multi_index实例都与一个特定账户的特定数据表进行交互(取决于实例化时的参数)。 +
+这个多索引表有几个特点:类似ORM中的映射表,行为独立的对象,列为属性;有主键和非主键,排序时默认为升序,同样主键只能唯一并为uint64_t类型;支持自定函数做为索引,但返回值受限,即只能为支持的键类型;允许多索引排序,但是二级索引不大于16,前面的代码可以看到,同时不支持二级索引的直接构建;类似双向链表可以双向迭代。 +
+它支持主要以下几种操作: +
+emplace:添加一个对象(row)到表中,返回一个新创建的主键迭代器。在这个过程中创建新对象,序列化写入表中,更新二级索引,付费。如果出现异常则直接抛出。 +
+erase:这个就简单了,直接擦除。可以用迭代器也可以引用对象来删除。删除后返回之后的迭代器,并更新相关索引及费用。 +
+modify:类似于数据库的UPDATE,这个比较麻烦,需要提供更新对象的迭代器,更新对象的引用,帐户(需要付费的)以及更新目标对象的函数(lambada),无返回值,在操作过程中主要是要对payer的属性进行判断,然后进行费用的计算和相关退费,完成后更新索引。 +
+
+get:由主键查找对象,返回对象的引用,如果没找到,抛出异常。 +
+find:根据主键查找已存在的对象。它的返回值是一个迭代器。如果没有查到返回一个end迭代器。 +
+迭代器有点类似于STD标准库的迭代器,可以前后遍历,这里不再赘述。 + +
+ +### 2、类boost::optional的自定义容器 +
+ +``` c++ + +/** + * @brief provides stack-based nullable value similar to boost::optional + * + * Simply including boost::optional adds 35,000 lines to each object file, using + * fc::optional adds less than 400. + */ +template +class optional +{ + public: + typedef T value_type; + typedef typename std::aligned_storage::type storage_type; + + optional():\_valid(false){} + ~optional(){ reset(); } + + optional( optional& o ) + :_valid(false) + { + if( o._valid ) new (ptr()) T( *o ); + \_valid = o._valid; + } + +...... + + template + optional( const optional& o ) + :_valid(false) + { + if( o._valid ) new (ptr()) T( *o ); + \_valid = o._valid; + } + + template + optional( optional& o ) + :_valid(false) + { + if( o._valid ) + { + new (ptr()) T( *o ); + } + \_valid = o._valid; + } + + template + optional( optional&& o ) + :_valid(false) + { + if( o._valid ) new (ptr()) T( fc::move(*o) ); + \_valid = o._valid; + o.reset(); + } + + ...... + + optional& operator=( optional&& o ) + { + if (this != &o) + { + if( \_valid && o._valid ) + { + ref() = fc::move(*o); + o.reset(); + } else if ( !\_valid && o._valid ) { + \*this = fc::move(*o); + } else if (\_valid) { + reset(); + } + } + return \*this; + } + + + friend bool operator < ( const optional a, optional b ) + { + if( a.valid() && b.valid() ) return \*a < \*b; + return a.valid() < b.valid(); + } +...... + + void reset() + { + if( \_valid ) + { + ref().~T(); // cal destructor + } + \_valid = false; + } + private: + template friend class optional; + T& ref() { return \*ptr(); } + const T& ref()const { return *ptr(); } + T* ptr() { return reinterpret_cast(&\_value); } + const T* ptr()const { return reinterpret_cast(&\_value); } + + bool _valid; + storage_type _value; +}; +``` +
+这个其实不能称做一个容器,因为它一般只盛放一个数据结构,它的主要目的标题也很清楚,其实是老大们不愿意使用BOOST的相关代码,太多了,这个才几百行,小巧实用。 +
+这个模板类的主要作用是封装一些数据结构,防止未初始化或者无意义的数据表达不清楚。比如一些返回值是NULL,有EOF,还有一些是string::npos等等,封装起来就是为了起一个标准的作用,其实你看这个类内部,并没有太多的真正意义的自己操作的数据,大多还是原生数据结构的使用。 +
+ +### 3、范围控制的scoped_exit +
+ +``` c++ +template +class scoped_exit { + public: + template + scoped_exit( C&& c ):callback( std::forward(c) ){} + + scoped_exit( scoped_exit&& mv ) + :callback( std::move( mv.callback ) ),canceled(mv.canceled) + { + mv.canceled = true; + } + + scoped_exit( const scoped_exit& ) = delete; + scoped_exit& operator=( const scoped_exit& ) = delete; + + ~scoped_exit() { + if (!canceled) + try { callback(); } catch( ... ) {} + } + + scoped_exit& operator = ( scoped_exit&& mv ) { + if( this != &mv ) { + ~scoped_exit(); + callback = std::move(mv.callback); + canceled = mv.canceled; + mv.canceled = true; + } + + return \*this; + } + + void cancel() { canceled = true; } + + private: + Callback callback; + bool canceled = false; +}; + +template +scoped_exit make_scoped_exit( Callback&& c ) { + return scoped_exit( std::forward(c) ); +} +``` +
+这个类其实也得很有趣,如果对RAII比较了解的话,这个其实有一点变相的意思,在离开某个范围时,调用这个数据结构的析构函数,然后调用指定的回调函数来处理一些相关的事情,比如清理一些内存等等。 +
+这里的模板构造函数用到了std::forward(c)完美转发,将左右值的匹配自动完成。一些小细节处理的相当不错。 +
+其实EOS中的数据结构和编程方式还是有些复杂的,特别是其中一些使用了比较传统的宏模板自动创建的方法(在MFC中常见,但广受诟病),所以一些代码还是比较晦涩的,不建议也这样使用。 +
diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\345\205\261\350\257\206.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\345\205\261\350\257\206.md" new file mode 100644 index 0000000..f9a2077 --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\205\255\345\205\261\350\257\206.md" @@ -0,0 +1,566 @@ +# eos源码分析之六共识 + +## 一、EOS使用的共识 +EOS使用的是与传统的共识方法不同的DPOS共识机制,而且在最新的版本中已经更改为了BFT-DPOS机制,在网上看到BM说他又找到了一种更新的共识机制,可以解决被超级节点控制的问题,不知道最终会是什么样子,在比特币和以太坊都使用POW的共识的前提下,EOS使用DPOS机制,可以说是解决高并发的一个比较好的方法。但是,DPOS机制很容易由于节点太少被攻击,事实上也是如此。那么什么是DPOS呢?EOS是怎么使用其进行块之间的共识的呢? +
+ 提到dpos,就不得不提到pos,PoS全称Proof of Stake,意为权益证明。说得直白一些就是谁存款多,存款时间长,谁就有权出块(记帐)。这个解决了POW一个痛点,即它不用挖矿,所以也不用耗费老多的电能。但是这这个算法有个致命问题,资本决定了一切,所以很容易被有钱人垄断。 +
+ DPOS比POS多了一个D,它的意义是授权,委托。二者的区别是,DPOS需要POS的持有者来通过选举代表,由代表实现出块。而在EOS中则有21个出块者(BP,BlcokProducer),或者叫超级节点。还有101个备份节点。当21个BP的15个确认交易后,交易即不可逆转。 +
+ +## 二、共识的过程 +
+ 1、初始化的共识 +
+ EOS初始启动是外在选举的21个超级节点,所以不涉及代码部分。但是一旦启动后会开始新的节点选举,选举成功后,将进行BFT-DPOS共识。 +
+ 2、选举 +
+ 主要的代码在contracts/social和eosio.system/voting.cpp中。在cleos的main.cpp中会发现几个数据结构体和相关的应用: +
+ + ``` c++ + + auto registerProducer = register_producer_subcommand(system); + auto unregisterProducer = unregister_producer_subcommand(system); + + auto voteProducer = system->add_subcommand("voteproducer", localized("Vote for a producer")); + voteProducer->require_subcommand(); + auto voteProxy = vote_producer_proxy_subcommand(voteProducer); + auto voteProducers = vote_producers_subcommand(voteProducer); + auto approveProducer = approve_producer_subcommand(voteProducer); + auto unapproveProducer = unapprove_producer_subcommand(voteProducer); + + auto listProducers = list_producers_subcommand(system); + + auto delegateBandWidth = delegate_bandwidth_subcommand(system); + auto undelegateBandWidth = undelegate_bandwidth_subcommand(system); + auto listBandWidth = list_bw_subcommand(system); + ``` +
+ 这些代码会驱动程序在启动后进行相应的动作。选举在EOS中其实也分成两类,即每人独自发起选举,也可以通过代理人代替自己选举,但结果就是本人就无法再投票了。相应的代码如下: +
+ + ``` c++ + /** + * @pre producers must be sorted from lowest to highest and must be registered and active + * @pre if proxy is set then no producers can be voted for + * @pre if proxy is set then proxy account must exist and be registered as a proxy + * @pre every listed producer or proxy must have been previously registered + * @pre voter must authorize this action + * @pre voter must have previously staked some EOS for voting + * @pre voter->staked must be up to date + * + * @post every producer previously voted for will have vote reduced by previous vote weight + * @post every producer newly voted for will have vote increased by new vote amount + * @post prior proxy will proxied_vote_weight decremented by previous vote weight + * @post new proxy will proxied_vote_weight incremented by new vote weight + * + * If voting for a proxy, the producer votes will not change until the proxy updates their own vote. + */ + //上面的介绍过程挺详细 + void system_contract::voteproducer( const account_name voter_name, const account_name proxy, const std::vector& producers ) { + require_auth( voter_name );//验证资格 + update_votes( voter_name, proxy, producers, true ); + } + void system_contract::update_votes( const account_name voter_name, const account_name proxy, const std::vector& producers, bool voting ) { + //validate input + if ( proxy ) {//判断是否为代理 + eosio_assert( producers.size() == 0, "cannot vote for producers and proxy at same time" ); + eosio_assert( voter_name != proxy, "cannot proxy to self" ); + require_recipient( proxy );//添加代理帐户 + } else { + eosio_assert( producers.size() <= 30, "attempt to vote for too many producers" ); + for( size_t i = 1; i < producers.size(); ++i ) { //验证英文注释中的排序 + eosio_assert( producers[i-1] < producers[i], "producer votes must be unique and sorted" ); + } + } + + //验证资格 + auto voter = \_voters.find(voter_name); + eosio_assert( voter \!= \_voters.end(), "user must stake before they can vote" ); /// staking creates voter object + eosio_assert( !proxy || !voter->is_proxy, "account registered as a proxy is not allowed to use a proxy" ); + + /* + * The first time someone votes we calculate and set last_vote_weight, since they cannot unstake until + * after total_activated_stake hits threshold, we can use last_vote_weight to determine that this is + * their first vote and should consider their stake activated. + \*/ + //计算权重,用来控制其抵押股权状态,并确定其是否为第一次投票 + if( voter->last_vote_weight <= 0.0 ) { + \_gstate.total_activated_stake += voter->staked; + if( \_gstate.total_activated_stake >= min_activated_stake ) { + \_gstate.thresh_activated_stake_time = current_time(); + } + } + + //计算权重 + auto new_vote_weight = stake2vote( voter->staked ); + if( voter->is_proxy ) {//是否代理 + new_vote_weight += voter->proxied_vote_weight; + } + + //处理投票 + boost::container::flat_map > producer_deltas; + if ( voter->last_vote_weight > 0 ) { + if( voter->proxy ) { + auto old_proxy = \_voters.find( voter->proxy ); + eosio_assert( old_proxy != \_voters.end(), "old proxy not found" ); //data corruption + \_voters.modify( old_proxy, 0, [&]( auto& vp ) {//投票后减去相应权重,对应英文注释 + vp.proxied_vote_weight -= voter->last_vote_weight; + }); + propagate_weight_change( *old_proxy ); //继续更新相关权重 + } else { + //非代理直接操作,一票三十投 + for( const auto& p : voter->producers ) { + auto& d = producer_deltas[p]; + d.first -= voter->last_vote_weight; + d.second = false; + } + } + } + + //处理得票 + if( proxy ) {//处理代理 + auto new_proxy = \_voters.find( proxy ); + eosio_assert( new_proxy != \_voters.end(), "invalid proxy specified" ); //if ( !voting ) { data corruption } else { wrong vote } + eosio_assert( !voting || new_proxy->is_proxy, "proxy not found" ); + if ( new_vote_weight >= 0 ) { + \_voters.modify( new_proxy, 0, [&]( auto& vp ) { + vp.proxied_vote_weight += new_vote_weight; + }); + propagate_weight_change( *new_proxy ); + } + } else { + if( new_vote_weight >= 0 ) { + for( const auto& p : producers ) { + auto& d = producer_deltas[p]; + d.first += new_vote_weight; + d.second = true; + } + } + } + + // 投票资格验证 + for( const auto& pd : producer_deltas ) { + auto pitr = \_producers.find( pd.first ); + if( pitr != \_producers.end() ) { + eosio_assert( !voting || pitr->active() || !pd.second.second /* not from new set */, "producer is not currently registered" ); + \_producers.modify( pitr, 0, [&]( auto& p ) { + p.total_votes += pd.second.first; + if ( p.total_votes < 0 ) { // floating point arithmetics can give small negative numbers + p.total_votes = 0; + } + \_gstate.total_producer_vote_weight += pd.second.first; + //eosio_assert( p.total_votes >= 0, "something bad happened" ); + }); + } else { + eosio_assert( !pd.second.second /* not from new set */, "producer is not registered" ); //data corruption + } + } + + //更新选举状态 + \_voters.modify( voter, 0, [&]( auto& av ) { + av.last_vote_weight = new_vote_weight; + av.producers = producers; + av.proxy = proxy; + }); +} + ``` +
+ 在前面的投票过程中发现,其实要想选举和成为出块者,都需要先行去注册,在最初的Main函数里也提到相应的子命令,那么看一下对应的代码: +
+ + ``` c++ + /** + * This method will create a producer_config and producer_info object for 'producer' + * + * @pre producer is not already registered + * @pre producer to register is an account + * @pre authority of producer to register + * + */ + void system_contract::regproducer( const account_name producer, const eosio::public_key& producer_key, const std::string& url, uint16_t location ) { + eosio_assert( url.size() < 512, "url too long" ); + eosio_assert( producer_key != eosio::public_key(), "public key should not be the default value" ); + require_auth( producer ); + + //查找是否已注册 + auto prod = \_producers.find( producer ); + + if ( prod != \_producers.end() ) { //已注册 + if( producer_key != prod->producer_key ) {//已注册,但KEY不同,即同名不同人,修改相关设置 + \_producers.modify( prod, producer, [&]( producer_info& info ){ + info.producer_key = producer_key; + info.is_active = true; + info.url = url; + info.location = location; + }); + } + } else {//全新加入 + \_producers.emplace( producer, [&]( producer_info& info ){ + info.owner = producer; + info.total_votes = 0; + info.producer_key = producer_key; + info.is_active = true; + info.url = url; + info.location = location; + }); + } + } +//找到相关,删除 + void system_contract::unregprod( const account_name producer ) { + require_auth( producer ); + + const auto& prod = \_producers.get( producer, "producer not found" ); + + \_producers.modify( prod, 0, [&]( producer_info& info ){ + info.deactivate(); + }); + } +//更新相关出块人 + void system_contract::update_elected_producers( block_timestamp block_time ) { + \_gstate.last_producer_schedule_update = block_time; + + auto idx = \_producers.get_index(); + + std::vector< std::pair > top_producers; + top_producers.reserve(21);//一票30投,但只取21,后49备用,再后忽略 + + for ( auto it = idx.cbegin(); it != idx.cend() && top_producers.size() < 21 && 0 < it->total_votes && it->active(); ++it ) { + top_producers.emplace_back( std::pair({{it->owner, it->producer_key}, it->location}) ); + } + + if ( top_producers.size() < \_gstate.last_producer_schedule_size ) { + return; + } + + /// sort by producer name + std::sort( top_producers.begin(), top_producers.end() ); + + std::vector producers; + + producers.reserve(top_producers.size()); + for( const auto& item : top_producers ) + producers.push_back(item.first); + + bytes packed_schedule = pack(producers); + + if( set_proposed_producers( packed_schedule.data(), packed_schedule.size() ) >= 0 ) { + \_gstate.last_producer_schedule_size = static_cast( top_producers.size() ); + } + } + /** + * An account marked as a proxy can vote with the weight of other accounts which + * have selected it as a proxy. Other accounts must refresh their voteproducer to + * update the proxy's weight. + * + * @param isproxy - true if proxy wishes to vote on behalf of others, false otherwise + * @pre proxy must have something staked (existing row in voters table) + * @pre new state must be different than current state + */ + //注册成代理人 +void system_contract::regproxy( const account_name proxy, bool isproxy ) { + require_auth( proxy ); + + auto pitr = \_voters.find(proxy); + if ( pitr != \_voters.end() ) { + eosio_assert( isproxy != pitr->is_proxy, "action has no effect" ); + eosio_assert( !isproxy || !pitr->proxy, "account that uses a proxy is not allowed to become a proxy" ); + \_voters.modify( pitr, 0, [&]( auto& p ) { + p.is_proxy = isproxy; + }); + propagate_weight_change( *pitr ); + } else { + \_voters.emplace( proxy, [&]( auto& p ) { + p.owner = proxy; + p.is_proxy = isproxy; + }); + } +} + ``` +
+ 分析投票及相关方法后,开始处理投票的交易动作: +
+ + ``` c++ + /** + * When a user posts we create a record that tracks the total votes and the time it + * was created. A user can submit this action multiple times, but subsequent calls do + * nothing. + * + * This method only does something when called in the context of the author, if + * any other contexts are notified + */ + void apply_social_post() { + const auto& post = current_action(); + require_auth( post.author ); + + eosio_assert( current_context() == post.author, "cannot call from any other context" ); + + static post_record& existing; + if( !Db::get( post.postid, existing ) ) + Db::store( post.postid, post_record( now() ) ); + } + + /** + * This action is called when a user casts a vote, it requires that this code is executed + * in the context of both the voter and the author. When executed in the author's context it + * updates the vote total. When executed + */ + void apply_social_vote() { + const auto& vote = current_action(); + require_recipient( vote.voter, vote.author ); + disable_context_code( vote.author() ); /// prevent the author's code from rejecting the potentially negative vote + + auto context = current_context(); + auto voter = vote.getVoter(); + + if( context == vote.author ) { + static post_record post; + eosio_assert( Db::get( vote.postid, post ) > 0, "unable to find post" ); + eosio_assert( now() - post.created < days(7), "cannot vote after 7 days" ); + post.votes += vote.vote_power; + Db::store( vote.postid, post ); + } + else if( context == vote.voter ) { + static account vote_account; + Db::get( "account", vote_account ); + auto abs_vote = abs(vote.vote_power); + vote_account.vote_power = min( vote_account.social_power, + vote_account.vote_power + (vote_account.social_power * (now()-last_vote)) / days(7)); + eosio_assert( abs_vote <= vote_account.vote_power, "insufficient vote power" ); + post.votes += vote.vote_power; + vote_account.vote_power -= abs_vote; + vote_account.last_vote = now(); + Db::store( "account", vote_account ); + } else { + eosio_assert( false, "invalid context for execution of this vote" ); + } + } + ``` +
+ 3、共识 +
+ 前面的选举过程其实就DPOS的过程,只不过,没有出块,体现不出来它的价值,在EOS的最新版本中采用了BFT-DPOS,所以看下面的数据结构: + +
+ + ``` c++ + struct block_header_state { +...... + uint32_t dpos_proposed_irreversible_blocknum = 0; + uint32_t dpos_irreversible_blocknum = 0; + uint32_t bft_irreversible_blocknum = 0; //BFT +...... + }; + +``` +
+ 这个变量bft_irreversible_blocknum是在push_confirmation中被赋值。connection::blk_send中广播。 +
+ +## 三、出块 +
+出块的代码主要在producer_plugin中: +
+ + ``` c++ + producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { + ...... + //省略各种出块条件的前期判断 + ....... + if (\_pending_block_mode == pending_block_mode::producing) { + // determine if our watermark excludes us from producing at this point + if (currrent_watermark_itr != \_producer_watermarks.end()) { + if (currrent_watermark_itr->second >= hbs->block_num + 1) { + elog("Not producing block because \"${producer}\" signed a BFT confirmation OR block at a higher block number (${watermark}) than the current fork's head (${head_block_num})", + ("producer", scheduled_producer.producer_name) + ("watermark", currrent_watermark_itr->second) + ("head_block_num", hbs->block_num)); + \_pending_block_mode = pending_block_mode::speculating; + } + } +} + +try { + uint16_t blocks_to_confirm = 0; + + if (\_pending_block_mode == pending_block_mode::producing) { + // determine how many blocks this producer can confirm + // 1) if it is not a producer from this node, assume no confirmations (we will discard this block anyway) + // 2) if it is a producer on this node that has never produced, the conservative approach is to assume no + // confirmations to make sure we don't double sign after a crash TODO: make these watermarks durable? + // 3) if it is a producer on this node where this node knows the last block it produced, safely set it -UNLESS- + // 4) the producer on this node's last watermark is higher (meaning on a different fork) + if (currrent_watermark_itr != \_producer_watermarks.end()) { + auto watermark = currrent_watermark_itr->second; + if (watermark < hbs->block_num) { + blocks_to_confirm = std::min(std::numeric_limits::max(), (uint16_t)(hbs->block_num - watermark)); + } + } + } + + chain.abort_block(); + chain.start_block(block_time, blocks_to_confirm);//调用真正的Controller.cpp出块 +} FC_LOG_AND_DROP(); +...... +} + //时间调度不断循环出块 + void producer_plugin_impl::schedule_production_loop() { + chain::controller& chain = app().get_plugin().chain(); + \_timer.cancel(); + std::weak_ptr weak_this = shared_from_this(); + + auto result = start_block();//出块 + + if (result == start_block_result::failed) { + elog("Failed to start a pending block, will try again later"); + \_timer.expires_from_now( boost::posix_time::microseconds( config::block_interval_us / 10 )); + + // we failed to start a block, so try again later? + \_timer.async_wait([weak_this,cid=++_timer_corelation_id](const boost::system::error_code& ec) { + auto self = weak_this.lock(); + if (self && ec != boost::asio::error::operation_aborted && cid == self->_timer_corelation_id) { + self->schedule_production_loop(); + } + }); + } else if (\_pending_block_mode == pending_block_mode::producing) { + \_timer.async_wait([&chain,weak_this,cid=++_timer_corelation_id](const boost::system::error_code& ec) { + auto self = weak_this.lock(); + if (self && ec != boost::asio::error::operation_aborted && cid == self->_timer_corelation_id) { + auto res = self->maybe_produce_block();//完成出块 + fc_dlog(\_log, "Producing Block #${num} returned: ${res}", ("num", chain.pending_block_state()->block_num)("res", res) ); + } + }); +...... +} else if (\_pending_block_mode == pending_block_mode::speculating && !\_producers.empty() && !production_disabled_by_policy()){ + // if we have any producers then we should at least set a timer for our next available slot + optional wake_up_time; + for (const auto&p: \_producers) { + auto next_producer_block_time = calculate_next_block_time(p); + if (next_producer_block_time) { + auto producer_wake_up_time = \*next_producer_block_time - fc::microseconds(config::block_interval_us); + if (wake_up_time) { + // wake up with a full block interval to the deadline + wake_up_time = std::min(\*wake_up_time, producer_wake_up_time); + } else { + wake_up_time = producer_wake_up_time; + } + } + } + + if (wake_up_time) { +....... + } else { + ...... + } + } else { + fc_dlog(\_log, "Speculative Block Created"); + } + } + + //在操作中断时启动异步出块 + bool producer_plugin_impl::maybe_produce_block() { + auto reschedule = fc::make_scoped_exit([this]{ + //退出本范围重新启动正常出块 + schedule_production_loop(); + }); + + try { + produce_block();//出块 + return true; + } FC_LOG_AND_DROP(); + + //处理异常时的出块 + fc_dlog(\_log, "Aborting block due to produce_block error"); + chain::controller& chain = app().get_plugin().chain(); + chain.abort_block(); + return false; + } + void producer_plugin_impl::produce_block() { +...... + + //idump( (fc::time_point::now() - chain.pending_block_time()) ); + chain.finalize_block();// 完成出块---下面是签名和提交块 + chain.sign_block( [&]( const digest_type& d ) { + auto debug_logger = maybe_make_debug_time_logger(); + return signature_provider_itr->second(d); + } ); + chain.commit_block(); +...... + } + ``` +
+ 真正的出块是在controller.hpp.cpp中,需要注意的是按照EOS一惯的风格,真正的代码在controller_impl类中: +
+ + ``` c++ + void start_block( block_timestamp_type when, uint16_t confirm_block_count, controller::block_status s ) { + FC_ASSERT( !pending ); + + FC_ASSERT( db.revision() == head->block_num, "", + ("db.revision()", db.revision())("controller_head_block", head->block_num)("fork_db_head_block", fork_db.head()->block_num) ); + + auto guard_pending = fc::make_scoped_exit([this](){ + pending.reset(); + }); + //创建pending,块在其中 + pending = db.start_undo_session(true); + + pending->_block_status = s; + + pending->_pending_block_state = std::make_shared( \*head, when ); // promotes pending schedule (if any) to active + pending->_pending_block_state->in_current_chain = true; + + pending->_pending_block_state->set_confirmed(confirm_block_count); + + auto was_pending_promoted = pending->_pending_block_state->maybe_promote_pending(); + + + //判断当前的状态并设置相关参数 + const auto& gpo = db.get(); + if( gpo.proposed_schedule_block_num.valid() && // if there is a proposed schedule that was proposed in a block ... + ( *gpo.proposed_schedule_block_num <= pending->_pending_block_state->dpos_irreversible_blocknum ) && // ... that has now become irreversible ... + pending->_pending_block_state->pending_schedule.producers.size() == 0 && // ... and there is room for a new pending schedule ... + !was_pending_promoted // ... and not just because it was promoted to active at the start of this block, then: + ) + { + // Promote proposed schedule to pending schedule. + if( !replaying ) { + ilog( "promoting proposed schedule (set in block ${proposed_num}) to pending; current block: ${n} lib: ${lib} schedule: ${schedule} ", + ("proposed_num", \*gpo.proposed_schedule_block_num)("n", pending->_pending_block_state->block_num) + ("lib", pending->_pending_block_state->dpos_irreversible_blocknum) + ("schedule", static_cast(gpo.proposed_schedule) ) ); + } + pending->_pending_block_state->set_new_producers( gpo.proposed_schedule ); + db.modify( gpo, [&]( auto& gp ) { + gp.proposed_schedule_block_num = optional(); + gp.proposed_schedule.clear(); + }); + } + + try { + //装填交易的实际数据 + auto onbtrx = std::make_shared( get_on_block_transaction() ); + push_transaction( onbtrx, fc::time_point::maximum(), true, self.get_global_properties().configuration.min_transaction_cpu_usage ); + } catch( const boost::interprocess::bad_alloc& e ) { + elog( "on block transaction failed due to a bad allocation" ); + throw; + } catch( const fc::exception& e ) { + wlog( "on block transaction failed, but shouldn't impact block generation, system contract needs update" ); + edump((e.to_detail_string())); + } catch( ... ) { + wlog( "on block transaction failed, but shouldn't impact block generation, system contract needs update" ); + } + + clear_expired_input_transactions();//清除相关交易 + update_producers_authority();//更新生产者相关的权限 + guard_pending.cancel();//解除锁 + } + + ``` + +
+ finalize_block 、sign_block、 commit_block 、abort_block等与签名和提交部分的代码都在这个模块中,就不再赘述,看代码就可以了。 +
diff --git "a/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\346\231\272\350\203\275\345\220\210\347\272\246.md" "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\346\231\272\350\203\275\345\220\210\347\272\246.md" new file mode 100644 index 0000000..a7b908f --- /dev/null +++ "b/eos/eos\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\346\231\272\350\203\275\345\220\210\347\272\246.md" @@ -0,0 +1,282 @@ +# eos源码分析之四智能合约 +智能合约和虚拟机部分会混合在一起讲,然后在各自的范围内偏向于哪个部分。 +
+ +# 一、一个简单智能合约 +
+智能合约的编译使用WASM来编译,也使用了一些自定义的代码用来固定智能合约的格式和入口等。智能合约产生二进制后会放到虚拟机中执行。首先看一个入门的智能合约,helloworld. +
+ +``` c++ +hello.cpp: + +#include +#include +usingnamespace eosio; +class hello :public eosio::contract +{ + public:using contract::contract; + /// @abi action + void helloworld( account_name user ) + { + print( "Hello, ", name{user} ); + } +}; +EOSIO_ABI( hello, (hi) ) +``` +
+在EOS的源码中最EOSIO_ABI被定义成: +
+ +``` c++ +#define EOSIO_ABI( TYPE, MEMBERS ) \ +extern "C" { \ + void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \ + auto self = receiver; \ + if( code == self ) { \ + TYPE thiscontract( self ); \ //注意这个变量,后面会引用 + switch( action ) { \ + EOSIO_API( TYPE, MEMBERS ) \ + } \ + eosio_exit(0); \ + } \ + } \ +} \ + +``` +
+这时候再对照一下EOS自带的一个空的智能合约的例子: +
+ +``` c++ +//noop.hpp +#pragma once + +#include +#include + +namespace noop { + using std::string; + /** + noop contract + All it does is require sender authorization. + Actions: anyaction*/ + class noop { + public: + + ACTION(N(noop), anyaction) { + anyaction() { } + anyaction(account_name f, const string& t, const string& d): from(f), type(t), data(d) { } + + account_name from; + string type; + string data; + + EOSLIB_SERIALIZE(anyaction, (from)(type)(data)) + }; + + static void on(const anyaction& act) + { + require_auth(act.from); + } + }; +} /// noop + +//noop.cpp +#include + +namespace noop { + extern "C" { + /// The apply method implements the dispatch of events to this contract + void apply( uint64_t receiver, uint64_t code, uint64_t action ) { + eosio::dispatch(code, action); + } + } +} + +``` +
+通过二者的对比可以发现,其实宏EOSIO_ABI自动完成了对action的映射分发。而EOS自带的则手动实现了静态分发,结果是一样的。它们的核心其实都是apply这个函数,如果有std::bind的使用经验,发现他们还是有些类似的。 +
+继续接着分析EOSIO_ABI的内部代码,里面调用了一个宏: +
+ +``` c++ +#define EOSIO_API_CALL( r, OP, elem ) \ + case ::eosio::string_to_name( BOOST_PP_STRINGIZE(elem) ): \ + eosio::execute_action( &thiscontract, &OP::elem ); \ + return; + +#define EOSIO_API( TYPE, MEMBERS ) \ + BOOST_PP_SEQ_FOR_EACH( EOSIO_API_CALL, TYPE, MEMBERS ) +``` +
+BOOST_PP_SEQ_FOR_EACH这个宏前面讲过,是按最后一个参数展开第一个宏。再看一执行的代码: +
+ +``` c++ +template +bool execute_action( T* obj, void (Q::*func)(Args...) ) { + size_t size = action_data_size(); + + //using malloc/free here potentially is not exception-safe, although WASM doesn't support exceptions + constexpr size_t max_stack_buffer_size = 512; + void* buffer = max_stack_buffer_size < size ? malloc(size) : alloca(size); + read_action_data( buffer, size ); + + auto args = unpack...>>( (char*)buffer, size ); + + if ( max_stack_buffer_size < size ) { + free(buffer); + } + + auto f2 = [&]( auto... a ){ + (obj->\*func)( a... ); //调用指定类对象的指定的函数,如果对照前面就是hello对象的helloworld + }; + + boost::mp11::tuple_apply( f2, args );//惰性求值 + return true; +} +``` +
+在bancor、currency的目录下,主要是货币转换相关的部分,dice是一个掷骰子的游戏的合约。***eosio.msig,eosio.token,eosio.bios*** 都是相关的智能合约的程序,可认为是EOS自带的智能合约或者说自带的软件。 +
+ +# 二、智能合约 +
+ +## 1、智能合约的内容 + +看完了上面的代码分析,回到智能合约本身来。智能合约是什么?有几部分?怎么执行? +
+EOS智能合约通过messages 及 共享内存数据库(比如只要一个合约被包含在transaction的读取域中with an async vibe,它就可以读取另一个合约的数据库)相互通信。异步通信导致的spam问题将由资源限制算法来解决。下面是两个在合约里可定义的通信模型: +
+1、Inline:Inline保证执行当前的transaction或unwind;无论成功或失败都不会有通知。Inline 操作的scopes和authorities和原来的transaction一样。 +
+2、Deferred: Defer将稍后由区块生产者来安排;结果可能是传递通信结果或者只是超时。Deferred可以触及不同的scopes,可以携带发送它的合约的authority*此特性在STAT不可用 +
+message 和Transaction的关系: +
+一个message代表一个操作,一个Transaction中可以包含一个或者多个message,合约和帐户通过其来通信。Message既可以单独发送也可以批量发送。 +
+ +``` +//单MESSAGE的Transaction +{ + "ref_block_num": "100", + "ref_block_prefix": "137469861", + "expiration": "2017-09-25T06:28:49", + "scope": ["initb","initc"], + "messages": [ + { + "code": "eos", + "type": "transfer", + "authorization": [ + { + "account": "initb", + "permission": "active" + } + ], + "data": "000000000041934b000000008041934be803000000000000" } + ], + "signatures": [], + "authorizations": [] +} + +//多Message的Transaction +{ + "ref_block_num": "100", + "ref_block_prefix": "137469861", + "expiration": "2017-09-25T06:28:49", + "scope": [...], + "messages": + [ + { + "code": "...", + "type": "...", + "authorization": [...], + "data": "..." + }, + { + "code": "...", + "type": "...", + "authorization": [...], + "data": "..." + }, ... + ], + "signatures": [], + "authorizations": [] +} + +``` + +
+ +## 2、Message名的限定和技术限制 + +Message的类型实际上是base32编码的64位整数。所以Message名的前12个字符需限制在字母a-z, 1-5, 以及'.' 。第13个以后的字符限制在前16个字符('.' and a-p)。 +
+另外需要注意的是,在合约中不得存在浮点数,所有的Transaction必须在1ms内执行完成,否则失败。从目前来看每个帐户每秒最多发出30个Transactions。 +
+ +## 3、智能合约的模块 +
+在前面的例程里可以看到在智能合约中有apply这个函数,也知道这个函数是非常重要的,其实还有别的几个函数也挺重要: +
+ +### init +init仅在被初次部署的时候执行一次。它是用于初始化合约变量的,例如货币合约中提供token的数量。 +
+ +### apply +apply是message处理器,它监听所有输入的messages并根据函数中的规定进行反馈。apply函数需要两个输入参数,code和 action。 +
+ +### code filter +为了响应特定message,您可以如下构建您的apply函数。您也可以忽略code filter来构建一个响应通用messages的函数。 +
+ +``` +if (code == N(${contract_name}) { + //响应特定message的处理器 +} +``` +
+在其中您可以定义对不同actions的响应。 +
+ +### action filter +为了相应特定action,您可以如下构建您的apply函数。常和code filter一起使用。 +
+ +``` +if (action == N(${action_name}) { + //响应该action的处理器 +} +``` +
+ +# 三、智能合约的编译 +
+EOS的智能合约必须使用EOSCPP这个命令来编译,任何需要布置在EOS上的智能合约必须编译成wasm(.wast)文件,并且有一个abi的文件。wasm-jit提供了这个编译的过程,在虚拟机的部分详细的介绍一下编译和执行的过程。 +
+ +# 四、智能合约的执行 + +在加载自定义的智能合约前,一般会加在上面提到的三个智能合约,用来测权限和相关的配置。这里看一看最基础的BIOS这个智能合约: +
+$ cleos set contract hello hello.wast hello.abi + +
+$ cleos push action hello helloworld '["fred" ]' -p hello +
+ +然后就可以在本地的nodeos节点的日志中查阅到上面的信息。 +
+ +# 五、智能合约的调试 + +参考EOS的github上的wiki的智能合约部分,其实上面有相当一部分就是从上面摘抄下来的。 + +
+
diff --git a/eos/img/eos_block_structure.png b/eos/img/eos_block_structure.png new file mode 100644 index 0000000..6e330ef Binary files /dev/null and b/eos/img/eos_block_structure.png differ diff --git a/eos/img/plugin_pic.png b/eos/img/plugin_pic.png new file mode 100644 index 0000000..800e130 Binary files /dev/null and b/eos/img/plugin_pic.png differ diff --git a/eos/img/roles-m.jpg b/eos/img/roles-m.jpg new file mode 100644 index 0000000..f5337d9 Binary files /dev/null and b/eos/img/roles-m.jpg differ diff --git a/eos/img/roles-s.jpg b/eos/img/roles-s.jpg new file mode 100644 index 0000000..6c54741 Binary files /dev/null and b/eos/img/roles-s.jpg differ diff --git a/ethereum/.gitkeep b/ethereum/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ethereum/cunzheng.md b/ethereum/cunzheng.md new file mode 100644 index 0000000..866c46c --- /dev/null +++ b/ethereum/cunzheng.md @@ -0,0 +1,263 @@ +# 基于以太坊区块链的电子存证应用 + +## 一、电子存证技术概述 + +传统的电子存证简单来说就是将源信息经过加密存储在一个具有公信力的独立第三方处,并绑定时间戳、创建人等信息用来证明在某个时间点存在这样的信息。举例来说,对于原创作品的保护,作者可以在创作完成后第一时间进行电子存证,以保证在以后出现侵权后证明自己最早创作了作品从而保护自己的权益。 + +电子存证的源信息可以是一段文本,文档,图片,视频等形式。对于这种各式各样的形式,电子存证一般存储的是源信息的哈希摘要;哈希是一段定长的比特串,类似于源信息的指纹,源信息只要改变,哈希就会和原来的完全不一样,由于哈希基本上是不可遍历的,所以在现实中可以认为哈希和源信息一一对应。哈希的这种特性普遍应用在文件指纹等场景,例如下载文件中的哈希校验。电子存证存储的哈希值是可以证明源信息真实未经篡改。哈希的另一个特性是无法从哈希摘要反推出原始信息,所以这样也保证了一些敏感信息的隐私性。 + +## 二、区块链电子存证的优势 + +对于传统的电子存证,具有公信力的独立第三方是一个很重要的角色;需要存证、取证、验证的各方都无条件信任。这样的第三方权限过于集中,如果第三方恶意修改数据,则基本无从查证;所以这样的第三方只能通过非技术的其他手段保证不去作恶。而区块链本身通过一环套一环的链式结构、分布式的存储、分布式的共识机制将这样过大的权利分散到所有参与者身上,保证了不产生这样一个权限过大的中心化第三方来具有作恶的可能。 + +通过区块链解决的存证中的信任问题,基于这样的一个前提,我们设计了基于以太坊的电子存证应用。 + +## 三、区块链存证合约设计 + +区块链上的数据经过矿工打包进区块中后基本上不可能更改,所以存证合约设计时候只需要做简单的读写操作。我们设计的存证合约读写的数据结构为: + +```solidity +struct Abstract { + uint timestamp; + address sender; + uint version; + bytes32 hash; + byte[512] extend; +} +``` + +数据结构中包含时间戳,调用存证合约的地址、存证的哈希值、扩展字段和标识版本的 version 字段。扩展字段和版本由使用方自定义编码和解码方式。通过这样的数据结构,构造一个 `mapping(bytes32 => Abstract)` Map 来用于分别保存存证信息,Map 结构的 Key 可以简单设置为存证的哈希值或者其他可追溯的值。 + +在这样结构上再封装对 Map 的读写操作就是一个简单的存证合约。可是由于区块链的特性,合约一旦上链后就不能更改了,所以如果合约逻辑出现漏洞就影响比较大,并且不能修复,重新部署合约又会丢失原有的数据,这样设计的合约是不可维护的。所以设计对这样的合约进行更改,将使用方直接调用 Map 操作的读写进行切断,在中间加入一个访问控制的合约层,这样经过修改的合约结构如下: + +**底层数据层合约**:仅封装对 Map 结构的读写操作,不设计具体的业务逻辑;在合约层加入权限控制,维护访问地址的白名单,仅白名单内部的地址具有操作合约数据的权限;仅合约部署者具有控制白名单的权限。 + +**上层逻辑合约**:封装了简单的存证业务逻辑,上层逻辑没有数据存储操作,在合约部署时候传入底层合约的地址作为参数,数据存储通过合约调用底层合约来实现。 + +这样分层后,一旦上层逻辑出现问题,可以通过管理员吊销上层合约的读写访问权限来阻止进一步的损失;合约升级是通过部署新的上层合约,赋予新的上层合约权限,吊销旧上层合约权限来实现;底层合约出现问题,也可以通过升级上层合约,在逻辑上绕过。 + +具体底层合约的代码如下: + +```solidity +pragma solidity ^0.4.17; + +contract DataModel { + struct Abstract { + uint timestamp; + address sender; + uint version; + bytes32 hash; + byte[512] extend; + } + + mapping(bytes32 => Abstract) abstractData; + mapping(address => bool) public allowedMap; + address[] public allowedArray; + + event AddressAllowed(address _handler, address _address); + event AddressDenied(address _handler, address _address); + event DataSaved(address indexed _handler, uint timestamp, address indexed sender, uint version, bytes32 hash); + event ExtendSaved(address indexed _handler, byte[512] extend); + event ExtendNotSave(address indexed _handler, uint version, byte[512] extend); + + function DataModel() public { + allowedMap[msg.sender] = true; + allowedArray.push(msg.sender); + } + + modifier allow() { + require(allowedMap[msg.sender] == true); + _; + } + + function allowAccess(address _address) allow public { + allowedMap[_address] = true; + allowedArray.push(_address); + AddressAllowed(msg.sender, _address); + } + + function denyAccess(address _address) allow public { + allowedMap[_address] = false; + AddressDenied(msg.sender, _address); + } + + function getData(bytes32 _key) public view returns(uint, address, uint, bytes32, byte[512]) { + return ( + abstractData[_key].timestamp, + abstractData[_key].sender, + abstractData[_key].version, + abstractData[_key].hash, + abstractData[_key].extend + ); + } + + function setData(bytes32 _key, uint timestamp, address sender, uint version, bytes32 hash) allow public { + abstractData[_key].timestamp = timestamp; + abstractData[_key].sender = sender; + abstractData[_key].version = version; + abstractData[_key].hash = hash; + DataSaved(msg.sender, timestamp, sender, version, hash); + } + + function setExtend(bytes32 _key, byte[512] extend) allow public { + if (abstractData[_key].version > 0) { + for (uint256 i; i < 512; i++) { + abstractData[_key].extend[i] = extend[i]; + } + ExtendSaved(msg.sender, extend); + } else { + ExtendNotSave(msg.sender, abstractData[_key].version, extend); + } + } +} +``` + +上层合约的代码如下: + +```solidity +pragma solidity ^0.4.20; + +import "./data-model.sol"; + +contract Storage { + DataModel dataModel; + uint currentVersion = 1; + + event StorageSaved(address handler, bytes32 indexed hashKey, uint timestamp, uint version, byte[512] extend); + + function Storage(address dataModelAddress) public { + dataModel = DataModel(dataModelAddress); + // require(dataModelAddress.delegatecall(bytes4(keccak256("allowAccess(address)")), this)); + } + + function getData(bytes32 key) public view returns(uint timestamp, address sender, uint version, bytes32 hashKey, string extend) { + byte[512] memory extendByte; + + (timestamp, sender, version, hashKey, extendByte) = dataModel.getData(key); + + bytes memory bytesArray = new bytes(512); + for (uint256 i; i < 512; i++) { + bytesArray[i] = extendByte[i]; + } + + extend = string(bytesArray); + return(timestamp, sender, version, hashKey, extend); + } + + function saveData(bytes32 hashKey, byte[512] extend) public { + dataModel.setData(hashKey, block.timestamp, msg.sender, currentVersion, hashKey); + dataModel.setExtend(hashKey, extend); + + StorageSaved(msg.sender, hashKey, block.timestamp, currentVersion, extend); + } +} +``` + +### 四、存证应用和以太坊区块链的交互 + +我们存证应用采用的是 Go 语言开发,通过 RPC 调用和链进行交互;由于采用 Go 语言开发,而正好以太坊官方提供 `go-ethereum` 的开源代码,所以以太坊 SDK 这块就直接选用这份开源代码;代码中不仅有主动调用 RPC 接口,而且需要接收节点推送的合约事件,所以 RPC 调用基于的是 WebSocket 协议,需要节点开启 WebSocket RPC 调用支持,可以通过启动参数 `--ws --wsaddr value --wsport value --wsapi value` 来实现[WIKI](https://github.com/ethereum/go-ethereum/wiki/Command-Line-Options),或者通过 JavaScript Console 的 [Admin API](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#admin_startws)来开启。 + +调用 Go SDK 的基本流程如下(代码省略错误处理等逻辑,仅保留核心流程): + +```go +import ( + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// 初始化 RPC 连接 +RPCClient, _ := rpc.Dial(conf.BlockChainConf.RPCUrl) + +// 初始化 ethclient +cli := ethclient.NewClient(RPCClient) + +// 导入 ABI 接口字符串 +parsedABI, _ := abi.JSON(strings.NewReader(evidenceABI)) + +// 初始化合约实例 +evidence := bind.NewBoundContract(conf.BlockChainConf.ContractAddress, parsedABI, cli, cli, nil) + +// 初始化上下文 +ctx, cancel := context.WithTimeout(context.Background(), conf.BlockChainConf.ConnTimeout) +defer cancel() + +// 交易签名私钥 +auth := bind.NewKeyedTransactor(account.PrivateKey) +auth.Context = ctx + +// 调用 RPC 发送存证合约交易 +tx, _ := evidence.Transact(auth, "saveData", hash, stringToBytes512(extend)) +``` + +最终返回的 tx 则为交易信息,这时候交易并没有即时出块,需要等待出块节点出块;这里通过监听合约的日志事件来实现: + +```go +// 订阅事件的过滤条件,这里传入合约的地址 +query := ethereum.FilterQuery{ + Addresses: []common.Address{conf.BlockChainConf.ContractAddress}, +} + +// Log 通道接收 +var logChan = make(chan types.Log) +ctx := context.Background() + +// 初始化客户端 +client, _ := blockchain.InitClient() + +// 初始化事件监听 +subscribe, _ := client.SubscribeFilterLogs(ctx, query, logChan) + +// 同样解析出 ABI 合约接口 +parsedABI, _ := abi.JSON(strings.NewReader(evidenceABI)) + +// 收到的事件结构,和合约代码中数据结构对应 +var receivedData struct { + Handler common.Address + HashKey common.Hash + Timestamp *big.Int + Version *big.Int + Extend Bytes512 +} + +for { + select { + case err := <-evt.Subscribe.Err(): + fmt.Printf("receive Error: %s\n", err.Error()) + case log := <-LogChan: + // 解包收到的 Log,receivedData 则为接收事件的数据 + err := parsedABI.Unpack(&receivedData, "StorageSaved", log.Data) + } +} +``` + +通过这样子,就可以在区块链出块后接受到事件,保证合约方法的成功调用 + +取证一种方式是通过调用合约的 `getData` 方法来做,和写入存证数据代码大同小异,如下: + +```go +// 对应存证的 evidence.Transact 方法 +err = evidence.Call(callOpts, &output, "getData", key) +``` + +另一种方式是通过合约的 Log 过滤来实现,如下: + +```go +// 这里过滤条件选用合约中 Map 数据的 Key +query := ethereum.FilterQuery{ + Topics: [][]common.Hash{[]common.Hash{}, []common.Hash{hashKey}}, +} + +// 调用 Client 的 FilterLogs 方法 +logs, err := client.FilterLogs(ctx, query) + +// 接着类似于监听事件那里,解包收到的 Log 得到数据 +``` + +### 五、结语 + +存证和区块链结合是一个和合适透明的场景,利用区块链解决的存证中存在的第三方信任问题;可是司法并没有跟上技术进步的节奏;存证现在还处于技术实现阶段,距离真正落地使用应该还有一段距离,这些都需要我们时刻关注相关信息。 diff --git a/ethereum/kademlia.md b/ethereum/kademlia.md new file mode 100644 index 0000000..e3ba1ae --- /dev/null +++ b/ethereum/kademlia.md @@ -0,0 +1,169 @@ +对于任意ASCII 字符a, 任意的单ASCII字符与a相同的前缀位(prefix)可以用异或计算, 计算结果对照表如下 + +prefix位长 | 可能的取值数目 | 异或结果范围 +------ | ------ | ------ + 8 | 0 | 0 + 7 | pow(2,0) | 1 + 6 | pow(2,1) | [2, 3] + 5 | pow(2,2) | [4, 7] + 4 | pow(2,3) | [8,15] + 3 | pow(2,4) | [16, 31] + 2 | pow(2,5) | [32, 63] + 1 | pow(2,6) | [64, 127] + 0 | pow(2,7) | [128, 255] + +举个栗子 若 a^b = 5, 对照上表的异或范围a,b 的相同前缀有5个, 验证下 a=248, b=253, a^b =5, bin(a) = "0b1111,1000", bin(b)="0b1111,1101" + +对于任意ASCII字符串a, 任意与其长度相同的ASCII字符串与a的前缀(prefix)位长可以通过迭代计算每个字节的累加得出, + +我们定义相同长度的ASCII字符串a,b 的距离为a的位长减前缀的位长 ++ 若a,b单字节, 则distance(a,b) = 8-length(prefix) ++ 若a,b是相同长度的字符串, 则distance(a, b) = length(a)*8 - length(prefix) +算法实现如下(golang版) + +```python +func prefixLength(xor uint8) uint { + switch { + case xor == 1: + return 7 + case xor >= 2 && xor <= 3: + return 6 + case xor >= 4 && xor <= 7: + return 5 + case xor >= 8 && xor <= 15: + return 4 + case xor >= 16 && xor <= 31: + return 3 + case xor >= 32 && xor <= 63: + return 2 + case xor >= 64 && xor <= 127: + return 1 + case xor >= 128 && xor <= 255: + return 0 + } + return 8 +} + +func calcDistance(a, b []byte) uint { + c := uint(0) + for i := 0; i < len(a) && i < len(b); i++ { + x := a[i] ^ b[i] + if x == 0 { + c += 8 + } else { + c += prefixLength(x) + break + } + } + return uint(len(a))*8 - c + +} +``` + +Kademlia算法作为路由算法来发现节点或者Topic的基本步骤分为三步 +1: 注册 Topic (可以是自己的ID或者想要分享给别人的某篇文章的关键字的hash值) +2: 广播 topic table, 同时保存接收到的table +3: 处理搜索请求(接受), +4: 返回找到目标topic对应的IP, 或者与目标topic 距离最近的几个点 +下面给出个简易的实现 + +```python +const ( + EntriesLength = 256 + NodesLengthPerEntry = 20 + ReplacementesLengthPerEntry = 20 + TopicMaxIdleTime = 10 * time.Second + EntryGCInterval = 20 * time.Second +) +type Topic [32]byte +type Node struct { + topic Topic // topic, nodeId..... + data []byte // 可以是ip或者其他的任何东西 + weight uint // topic 的权重, 最简单的可以通过计算与目标机的链接时间得出 + lastUsed time.Time // slot满的时候用来删除 +} +type Nodes []*Node // 按照weight排序的list需要实现heap.Interface +//implement heap interface, meth: Push/Pop/Less/Swap/Len + +type entry struct { + nodes *Nodes + replacementes *Nodes + lastGCTime time.Time +} + +type Table struct { + topic Topic //nodeID + entries []*entry // topic table , length等于最远距离, 通过计算接受的topic与自己的topic 计算距离, 最远距离为8 * len(topic) +} + +func NewTable(t Topic) *Table { +// initialize +} + +func (tbl *Table) Add(t Topic, weight uint, data []byte) { + dis := calcDistance(tbl.topic, t) + tbl.entries[dis].add(t, weight, data) +} + +func (e *entry) add(t Topic, weight uint, data []byte) { + n := new(Node) + n.data = data + n.topic = t + n.lastUsed = time.Now() + n.weight = weight + if e.nodes.Len() < NodesLengthPerEntry { + heap.Push(e.nodes, n) + return + } + if e.replacementes.Len() > 0 { + if (*(e.nodes))[0].weight < weight { + nn := heap.Pop(e.nodes) + heap.Push(e.nodes, n) + n = nn.(*Node) + } + } + heap.Push(e.replacementes, n) + if e.replacementes.Len() == ReplacementesLengthPerEntry { + heap.Pop(e.replacementes) + } + go e.gc() +} + +func (e *entry) gc() { + // 需要删除太老的节点信息了 +} + +func distcmp(target, a, b Topic) int { + for i := range target { + da := a[i] ^ target[i] + db := b[i] ^ target[i] + if da > db { + return 1 + } else if da < db { + return -1 + } + } + return 0 +} + +type disCalactor struct { + entries []*Node + target Topic +} + +func (d *disCalactor) push(n *Node, maxElems int) { + ix := sort.Search(len(d.entries), func(i int) bool { + return distcmp(d.target, d.entries[i].topic, n.topic) > 0 + }) + if len(d.entries) < maxElems { + d.entries = append(d.entries, n) + } + if ix < len(d.entries) { + copy(d.entries[ix+1:], d.entries[ix:]) + d.entries[ix] = n + } +} + +``` + + diff --git "a/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225.pdf" "b/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225.pdf" new file mode 100644 index 0000000..1f4004d Binary files /dev/null and "b/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225.pdf" differ diff --git "a/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225\346\216\250\350\215\220\345\217\202\346\225\260.pdf" "b/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225\346\216\250\350\215\220\345\217\202\346\225\260.pdf" new file mode 100644 index 0000000..bd6b70c Binary files /dev/null and "b/fabric-bccsp/SM2\346\244\255\345\234\206\346\233\262\347\272\277\345\205\254\351\222\245\345\257\206\347\240\201\347\256\227\346\263\225\346\216\250\350\215\220\345\217\202\346\225\260.pdf" differ diff --git "a/fabric-bccsp/SM3\345\257\206\347\240\201\346\235\202\345\207\221\347\256\227\346\263\225.pdf" "b/fabric-bccsp/SM3\345\257\206\347\240\201\346\235\202\345\207\221\347\256\227\346\263\225.pdf" new file mode 100644 index 0000000..c6a02cb Binary files /dev/null and "b/fabric-bccsp/SM3\345\257\206\347\240\201\346\235\202\345\207\221\347\256\227\346\263\225.pdf" differ diff --git "a/fabric-bccsp/SM4\345\210\206\347\273\204\345\257\206\347\240\201\347\256\227\346\263\225.pdf" "b/fabric-bccsp/SM4\345\210\206\347\273\204\345\257\206\347\240\201\347\256\227\346\263\225.pdf" new file mode 100644 index 0000000..3f99b81 Binary files /dev/null and "b/fabric-bccsp/SM4\345\210\206\347\273\204\345\257\206\347\240\201\347\256\227\346\263\225.pdf" differ diff --git "a/fabric-bccsp/fabric\344\270\255\344\275\277\347\224\250\345\233\275\345\257\206.md" "b/fabric-bccsp/fabric\344\270\255\344\275\277\347\224\250\345\233\275\345\257\206.md" new file mode 100644 index 0000000..9685fe3 --- /dev/null +++ "b/fabric-bccsp/fabric\344\270\255\344\275\277\347\224\250\345\233\275\345\257\206.md" @@ -0,0 +1,197 @@ +# fabric中使用国密(SMx) + +## 一、fabric中的加密体系 +### 1、整体架构 +包的基本架构: +
+ +![p](img/bccsp-p.png) +类的基本架构: +
+ +![c](img/bccsp-c.png) + + +### 2、国密的接入 +国密的接入在目前找到资料有两种解决方案,一种是完全在Fabric层面上解决,优势是形式简单,但证书部分可能需要单独处理。另外一种是直接修改Go的基础库,用国密的相关算法替换掉基础库中的相关实现。这个优势在于Fabric整体上不需要怎么改动。但缺点是丧失了移植性。 + +### 3、相关接口 +工厂的相关接口: +
+ +```go +type BCCSP interface { + + // KeyGen generates a key using opts. + KeyGen(opts KeyGenOpts) (k Key, err error) + + // KeyDeriv derives a key from k using opts. + // The opts argument should be appropriate for the primitive used. + KeyDeriv(k Key, opts KeyDerivOpts) (dk Key, err error) + + // KeyImport imports a key from its raw representation using opts. + // The opts argument should be appropriate for the primitive used. + KeyImport(raw interface{}, opts KeyImportOpts) (k Key, err error) + + // GetKey returns the key this CSP associates to + // the Subject Key Identifier ski. + GetKey(ski []byte) (k Key, err error) + + // Hash hashes messages msg using options opts. + // If opts is nil, the default hash function will be used. + Hash(msg []byte, opts HashOpts) (hash []byte, err error) + + // GetHash returns and instance of hash.Hash using options opts. + // If opts is nil, the default hash function will be returned. + GetHash(opts HashOpts) (h hash.Hash, err error) + + Sign(k Key, digest []byte, opts SignerOpts) (signature []byte, err error) + + // Verify verifies signature against key k and digest + // The opts argument should be appropriate for the algorithm used. + Verify(k Key, signature, digest []byte, opts SignerOpts) (valid bool, err error) + + // Encrypt encrypts plaintext using key k. + // The opts argument should be appropriate for the algorithm used. + Encrypt(k Key, plaintext []byte, opts EncrypterOpts) (ciphertext []byte, err error) + + // Decrypt decrypts ciphertext using key k. + // The opts argument should be appropriate for the algorithm used. + Decrypt(k Key, ciphertext []byte, opts DecrypterOpts) (plaintext []byte, err error) +} +``` +
+执行类的具体接口: +
+ +```go +type KeyGenerator interface { + // KeyGen generates a key using opts. + KeyGen(opts bccsp.KeyGenOpts) (k bccsp.Key, err error) +} +// KeyDeriver is a BCCSP-like interface that provides key derivation algorithms +type KeyDeriver interface { + KeyDeriv(k bccsp.Key, opts bccsp.KeyDerivOpts) (dk bccsp.Key, err error) +} +// KeyImporter is a BCCSP-like interface that provides key import algorithms +type KeyImporter interface { + KeyImport(raw interface{}, opts bccsp.KeyImportOpts) (k bccsp.Key, err error) +} +// Encryptor is a BCCSP-like interface that provides encryption algorithms +type Encryptor interface { + Encrypt(k bccsp.Key, plaintext []byte, opts bccsp.EncrypterOpts) (ciphertext []byte, err error) +} + +// Decryptor is a BCCSP-like interface that provides decryption algorithms +type Decryptor interface { + Decrypt(k bccsp.Key, ciphertext []byte, opts bccsp.DecrypterOpts) (plaintext []byte, err error) +} + +// Signer is a BCCSP-like interface that provides signing algorithms +type Signer interface { + + Sign(k bccsp.Key, digest []byte, opts bccsp.SignerOpts) (signature []byte, err error) +} + +// Verifier is a BCCSP-like interface that provides verifying algorithms +type Verifier interface { + Verify(k bccsp.Key, signature, digest []byte, opts bccsp.SignerOpts) (valid bool, err error) +} + +// Hasher is a BCCSP-like interface that provides hash algorithms +type Hasher interface { + Hash(msg []byte, opts bccsp.HashOpts) (hash []byte, err error) + GetHash(opts bccsp.HashOpts) (h hash.Hash, err error) +} +``` +可以看到,为了保持一致性和解耦,两类接口的实现基本没有差别。 + +## 二、国密算法的介绍 +### 1、国密算法的分类 +国密算法即国家密码局认定的国产密码算法,主要有SM1(SCB2)、SM2、SM3、SM4、SM7、SM9、祖冲之密码算法(ZUC),其中SM2,SM3,SM4三种算法是比较常接触到的。在密码学中,一般分为对称加密算法和非对称加密算法,国密的基本分类如下: +
+对称算法:SM1,SM4,SM7,ZUC(SM1,SM7算法不公开) +
+非对称算法:S2,S9是非对称算法 +
+哈希算法:SM3 +
+### 2、国密算法及应用 +SM1算法:分组密钥算法,分组长度是128位。密钥长度也是128位。以IP核的形式存在于芯片中。它主要应用于需要智能IC卡,加密卡等的电子政务和电子商务的环境中。(诸如公安、金融等)。 +
+SM2算法:比较熟悉的椭圆曲线公钥密码算法,包括三部分:椭圆曲线数字签名算法SM2-1,椭圆曲线密钥交换协议SM2-2,椭圆曲线公钥加密算法SM2-3。算法使用的是ECC椭圆曲线密码机制,但在签名和密钥交换上不同于ECDSA、ECDH等国际标准,而是采用了更安全的方式(比如签名使用SM3的杂凑算法等),整体性能优于RSA算法。同时,其还推荐了一条256位的曲线做为标准曲线(曲线的使用会导致算法的安全性的强度,目前美国提供的几条曲线,很多密码专家怀疑有漏洞)。 +
+SM3算法:密码杂凑算法。它主要在SM2,SM9中使用。其实就是常见的哈希和散列算法。应用范围主要是商用密码中的数字签名和验证以及随机数的生成。 +
+SM4算法:分组加密算法,密钥长度和分组长度都是128位。采用32轮非线性迭代结构。它主要用于无线局域网产品。 +
+SM7算法:分组密码算法,分组和密钥长度都是128位,适用于非接触式IC卡,比如门禁、一卡通等。 +
+SM9算法:标识密码算法,主要是降低公开密钥系统中的密钥和证书管理的复杂性。使用用户的特征标识(如电邮、电话号码等)作为公钥,省略了交换数字证书和公钥的过程。它主要应用于端对端的离线安全通讯、云端数据的加密等等。 +
+ZUC:祖冲之算法,中国自主的流密码算法,主要应用于移动通信4G网络中的加密。它分为三部分:祖冲之算法,加密算法和完整性算法。 +
+国密相关的文档官方下载地址在文末,一个是国家标准公开下载的地址,一个是国家密码管理局相关地址,二者提供的是一致的。 +
+ +## 三、国密算法的相关资料 +在对国密算法库在Fabric中的应用进行调研后,发现,主流有三种情况,即直接用go语言完成密码库,或者在相关的版本上进行Patch,最后就是使用传统的c++/c来实现库。这种目前针对国密的硬件算法SM1居多。其中北京大学信息安全实验室的开源项目比较有名。 +### 1、Go国密算法库 +国密算法本身的实现比较流行的还是以c/c++为主,但是其它语言的也不少。其中Go语言的以苏州同济区块链研究院的国密算法库比较有影响。其地址见文末的相关说明。它同时提供了单独的Go国密算法库和直接用其替换的Fabric相关版本。 +### 2、补丁算法库 +其实说补丁算法库并没有什么特别意义,只是说应用的一种形式而已,类似于修改Linux内核后给出一个Patch。地址见文末。 + +### 3、c/c++算法库 +北大的GsmSSL,是开源中国推荐的六大加密项目之一。但是它不是专门为Fabric开发的,应该是为SSL开发的。它的接口相当丰富,Go,Java等语言都有。具体地址见文末。 + +### 4、对比 +使用Go的算法库的优势在于有完全开源并集成好的代码,进一步来讲,甚至可以直接使用其在国密基础上的Fabric,使用补丁也非常简单,但可能会产生版本的依赖的问题。使用c++相关的库,需要自己处理算法的Go和c的接口,同时对BCCSP中的相关的接口和框架流程也需要重新进行编写。所以在实际应用中要考虑具体的实际情况。 + +## 四、国密的应用 + +### 1、直接替换接口工厂 +在上面的Fabric的加密体系分析中,在BCCSP中,实现了加密体系的模块化和接口化。最简单的替换方法就是直接将生成工厂内的原有的接口替换成新的SM系列算法,这样改动最小,但是可能会引起一些歧义。 + +### 2、实现接口工厂 +如果想同时维护多套的密码体系,可以在BCCSP中重新定义一套加密体系的工厂类,继承并实现相关的加密接口,但是这样就需要修改整个Fabric中相关应用的代码。这样做的优势在于,清楚明了,便于同时维护多套密码系统。 + +## 五、分析总结 +国密的SM系列算法在国内的一些重要的行业是必不可少的,这既是安全需要也是一种强制行为。针对Fabric在金融、司法等行业的应用,SM算法的引入是不可或缺的。Fabric在设计之初可能就考虑到了类似的问题,所以其加密系统接口的可插拔化设计是非常有前瞻性的,值得借鉴。 +
+在这次的国密的相关资料调研整理过程中,将一些相关的国家标准文档及库的源码统一整理提交到Github的开源库中,供大家参考,地址见文末。 + +## 六、附属资料 +本文件相关资源Github地址: +
+https://github.com/XChainLab/documentation/blob/master/fabric-bccsp/fabric%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%9B%BD%E5%AF%86.md +
+国密系列《国家标准全文公开系统》地址: +
+http://www.gb688.cn/bzgk/gb/std_list_type?r=0.9349212631727359&p.p1=2&p.p2=SM4&p.p90=circulation_date&p.p91=desc +
+国家密码管理局相关: +
+http://www.oscca.gov.cn/sca/xxgk/bzgf.shtml +
+国密改造系列相关文章源码: +
+https://blog.csdn.net/dyj5841619/article/details/90670703 +
+https://github.com/dddengyunjie/fabric +
+苏州同济区块链研究院相关文章源码: +
+https://www.cnblogs.com/laolieren/p/hyperledger_fabric_gm_summary.html +
+https://github.com/tjfoc/fabric-ca-gm +
+国密补丁方式文章源码: +
+https://www.blockchainbrother.com/article/19680 +
+https://github.com/flyinox/fabric-sm-patch/blob/master/fabric-sm-patch +
+北京大学信息安全实验室的开源项目 +
+https://github.com/guanzhi/GmSSL +
diff --git a/fabric-bccsp/img/bccsp-c.png b/fabric-bccsp/img/bccsp-c.png new file mode 100644 index 0000000..412002f Binary files /dev/null and b/fabric-bccsp/img/bccsp-c.png differ diff --git a/fabric-bccsp/img/bccsp-p.png b/fabric-bccsp/img/bccsp-p.png new file mode 100644 index 0000000..b2169d3 Binary files /dev/null and b/fabric-bccsp/img/bccsp-p.png differ diff --git a/fabric/Fabric.block.data.structure.md b/fabric/Fabric.block.data.structure.md new file mode 100644 index 0000000..b769c05 --- /dev/null +++ b/fabric/Fabric.block.data.structure.md @@ -0,0 +1,161 @@ + + +# Hyperledger Fabric Block Data Structure + + + +## 前言 +区块链上所有的一切都是围绕着数据处理来展开的,无论是架构设计,存储,加密/解密,共识,智能合约,账本,P2P,等等。而终极的数据就是存储在磁盘上的区块链数据。所以理解链上数据的结构对于完整理解一个平台的设计至关重要。这里我们就来解读一下Hyperledger Fabric的区块链数据结构。 + +## Protobuf +Protobuf是Google开发的一种用于数据交换和传输的数据格式,Fabric大量使用了Protobuf来做数据传输和存储,包括在内存中运行的数据也有很多是采用了Protobuf来做序列化,对于Protobuf这里不做详细介绍。下面是Protobuf在Github上的链接,这个是Golang版本的。 + +https://github.com/golang/protobuf + +## 整体数据结构定义图 + +让我们先来看一张整体的数据结构定义图,以便有一个整体的概念 + +![](./images/fabric.block.data.structure.png) + +## Block 定义 +Block是区块链数据结构里面的一个最基本的元素,其数据结构的定义位于下面这个Protobuf文件中,使用Protobuf格式定义,然后再使用protobuf工具生成golang的源文件 + +`/fabric/protos/common/common.proto` 对应的golang源文件是:`/fabric/protos/common/common.pb.go` + +```go +type Block struct { + Header *BlockHeader + Data *BlockData + Metadata *BlockMetadata +} +``` +从这个结构体的定义可以看出,一个Block里面包含了三种类型的数据, +- Header,即区块头,这里又包含了三项数据,包括: + - Number:Block Number,即区块号 + - PreviousHash:前一个区块头的hash值 + - DataHash:Data域的hash值,这里没有包含Metadata +- Data,即区块数据,这里包含的就是所有的Transaction数据 +- Metadata,元数据,这里记录的是一些辅助信息,包括: + - Metadata[BlockMetadataIndex_SIGNATURES]:签名信息 + - Metadata[BlockMetadataIndex_LAST_CONFIG]:Channel的最新配置区块索引 + - Metadata[BlockMetadataIndex_TRANSACTIONS_FILTER]:交易是否合法的标记 + - Metadata[BlockMetadataIndex_ORDERER]:Channel的排序服务信息 + +这里是BlockHeader结构体的定义,这里面的三项数据前面已经有说明了: +```go +type BlockHeader struct { + Number uint64 + PreviousHash []byte + DataHash []byte +} +``` + +这里是BlockMetadata的定义: +```go +type BlockMetadata struct { + Metadata [][]byte +} +``` +我们可以看到,BlockMetadata结构体中的Metadata域是一个byte类型的二维数组,而这个数据是由下面的Metadata结构体序列化而来的 +```go +type Metadata struct { + Value []byte + Signatures []*MetadataSignature +} +``` +从这个定义中可以知道,元数据就是由一个value和相应的签名组成 + + +### BlockData 定义 +下面我们再来看一个Block中最核心的data部分的定义 + +这个结构体只有一个数据Data,又是一个byte类型的二维数组。和前面的Metadata一样,这个也是由另外一个数据结构序列化而来的。 +```go +type BlockData struct { + Data [][]byte +} + +type Envelope struct { + Payload []byte + Signature []byte +} +``` +在Envelope这个结构体中,Signature就是对Payload数据的签名,这个数据在网络中传输的时候,这个签名用于对数据做有效性验证。Payload本身又是由另外一个数据结构序列化而来的 +```go +type Payload struct { + Header *Header + Data []byte +} +``` +Payload,中文直译是负载的意思,也就是说具体承载交易数据。这里包含一个header,这个header带有类型,负责描述这个payload的性质以及如何解析data字段。 +另外header中还包含有创建者的信息和一个随机数,以及用来标识逻辑时间窗口的时期信息,这个时间窗口数据用于抵御重放攻击。 + +同样,Header中的ChannelHeader字段是由一个ChannelHeader的结构序列化而来的。 +```go +type Header struct { + ChannelHeader []byte + SignatureHeader []byte +} + +type ChannelHeader struct { + Type int32 + Version int32 + Timestamp *google_protobuf.Timestamp + ChannelId string + TxId string + Epoch uint64 + Extension []byte + TlsCertHash []byte +} +``` +* Type:ChannelHeader中的Type,定义了这个Payload包含的是那种类型的消息,其中,1~10000的数值定义由系统保留。目前有如下的类型定义: + * MESSAGE = 0; // 不透明的消息类型 + * CONFIG = 1; // Channel配置消息 + * CONFIG_UPDATE = 2; // Channel配置更新的交易 + * ENDORSER_TRANSACTION = 3; // 客户端通过SDK向Endorser节点提交交易提案 + * ORDERER_TRANSACTION = 4; // 排序节点内部使用 + * DELIVER_SEEK_INFO = 5; // 用于指示Deliver API查找信息 + * CHAINCODE_PACKAGE = 6; // 用户安装链码的时候打包链码组件 + * PEER_RESOURCE_UPDATE = 7; // 用于peer资源更新的时候,对更新信息做编码 + + +* Version:指定消息协议的版本号 +* Timestamp:定义发件人发出消息时候的本地时间,这里使用了Google Protobuf框架里面定义的时间格式 +* ChannelId:指定了这个消息是绑定在哪一个Channel上的。我们知道Fabric中定义了Multi Channel的概念,绑定到特定的Channel上面,意味着这个消息只能由这个特定的Channel处理,其他的Channel是不能查看这个消息的 +* TxId:一个唯一的ID,通常由更高层设定,比如用户或者SDK。这个数据传递给背书节点的时候,背书节点会检查其唯一性。当消息被正确传递的时候,记账节点也会去检索这条消息,同时也会去检查其唯一性。最终,这个数据会被保存在账本中 +* Epoch:这个数据基于区块的高度(区块号)来定义,指定了这条消息的时间窗口,只有在满足以下两个条件的情况下,目的节点才会接受这条消息: + * 消息中指定的时间信息是当前时期 + * 在这一个时间段内,这条消息只出现了一次(预防重放攻击) +* Extension:根据header type,不同的特定消息会附加自己特定的扩展数据 +* TlsCertHash:如果使用了共同TLS,则这个数据指示了客户端的TLS证书 + + +### Transaction 定义 +后续Transaction的定义则是在这个proto文件中,`/fabric/protos/peer/transaction.proto` +对应的golang源文件是 `/fabric/protos/peer/transaction.pb.go` + +在Payload结构体中的Data,是由Transaction结构体序列化而来的, +```go +type Transaction struct { + Actions []*TransactionAction +} +``` +客户端发送到排序服务节点的数据就是由这个Transaction结构体定义的,一个Transaction结构又包含了一个或者多个TransactionAction数据。每一个TransactionAction数据就是一个交易提案,可能包含有多个Action。Transaction结构数据是原子性的,这就意味着,一条Transaction结构数据中包含的所有Action要么是全部被写入账本,要么是全部丢弃。这里需要注意的是:一条Transaction结构数据中可能会包含有多个Header,但是所有这些Header的Creator都必须是相同的,这也就意味着所有这些Action都是由同一个用户发起的。 + +一个单独的客户端可以提交一系列相互独立的提案,每一个提案都可以包含它自己的header和Payload(ChaincodeProposalPayload)。背书节点会对每一个提案单独做背书并且产生独立的Action + +每一个独立的背书节点都会给Payload(ChaincodeProposalPayload)打上自己的签名,任意数量的提案以及这些提案包含的Action都可以打包到一条Transaction数据之中,当然这样的一条Transaction数据会被当做一条原子数据来处理。 + +Transaction结构体本身是由TransactionAction的数组组成 +```go +type TransactionAction struct { + Header []byte + Payload []byte +} +``` + +这个结构体为每一个Action绑定了一个提案,在Header字段中的type指定了这个action的分类 + + +这个结构体中的Payload具体类型则是由Header中的type定义的,它是ChaincodeActionPayload结构体的序列化数据 diff --git "a/fabric/Fabric\346\236\266\346\236\204\350\247\243\350\257\273.md" "b/fabric/Fabric\346\236\266\346\236\204\350\247\243\350\257\273.md" new file mode 100644 index 0000000..e1233f8 --- /dev/null +++ "b/fabric/Fabric\346\236\266\346\236\204\350\247\243\350\257\273.md" @@ -0,0 +1,224 @@ + + +# Hyperledger Fabric 架构解读 + + +## 前言 +Hyperledger Project由Linux基金会创办于2015年10月,是一个开源的区块链研发孵化项目,致力于提供可协同开发以区块链为底层的分布式账本。旗下的Fabric项目目标为打造一个提供分布式账本解决方案的联盟链平台。 + + + +## Fabric 特点 + +##### 1. 完备的权限控制和安全保障 + +成员必须被许可才能加入网络,通过证书,加密,签名等手段保证安全。通过多通道功能,保证只有参与交易的节点能访问到数据,其他的节点看不到。满足数据保护方面的法律法规要求。如有些行业,需要知道谁访问了特定的数据。 + +##### 2. 模块化设计,可插拔架构 + +如状态数据库可采用Level DB或者Couch DB,或其他的key-value数据库。身份管理(identity management)可以采用自己的。共识机制和加密算法也是可插拔的,可以根据实际情况选择替换。 + +##### 3. 高性能,可扩展 + +Fabric采用模块化架构把交易处理划分为3个阶段:通过Chaincode进行分布式业务逻辑处理和协商(peers);交易排序(orderers);交易的验证和提交(committers)。这样划分带来的好处:不同的阶段由不同的节点角色参与,不需要全网的节点都参与。网络的性能和扩展性得到优化。Peer节点和Orderer节点可以独立扩展,并可以动态增加。 + +##### 4. 在不可更改的分布式账本上提供丰富的查询功能 + +可以在Level DB上进行按Key查询,按复合Key查询,按Key的范围查询。如果采用Couch DB,Couch DB是文档数据库,数据是JSON格式的。除了支持按Key查询,按复合Key查询,按Key的范围查询外,还支持全文搜索。 + + + +-------------------------- + +## 逻辑架构图 + +如下图所示,整体上来说,Fabric架构分为上下两层, +* 上面一层是应用层,是应用程序与Fabric进行交互的媒介,包括身份管理,账本处理,交易处理和智能合约 +* 下面一层是核心的区块链实现,包括成员管理,共识服务,分布式账本,排序服务,P2P网络,链码服务,安全和加密服务等等 + + +![](./images/fabric.logic.architecture.jpg) + + +#### 一、应用层角度 + +##### Identity(身份管理) +Fabric的设计目标是联盟链而不是公链,这个目标决定了Fabric在用户管理上和Bitcoin等公链有很大的不同。需要充分的考虑到商业应用对安全、隐私、监管、审计、性能等方面的需求,成员必须被许可才能加入网络。这个模块为整个区块链网络提供身份管理、隐私、保密和可审计的服务。 + +##### Ledger(账本管理) +应用程序对于账本的管理主要是两种类型,一种是数据的读取,另一种是数据写入。只有获得授权的用户才可以查询账本数据,并且有多种可以使用的查询方式,包括使用区块号查询区块,使用区块hash查询区块,使用交易ID查询交易。还可以根据Channel的名字查询区块链信息 + +##### Transactions(交易管理) +应用程序对于数据的写入,只能通过发起一个交易的方式来完成。Fabric SDK提供了相应的Interface,应用程序调用SDK接口,通过交易管理提交交易提案(Transaction Proposal),应用程序收集到经过背书(Endorsement)之后的交易之后,通过广播发送给排序服务(Orderer)节点,经过排序之后,生成区块。 + +##### Smart Contract(智能合约) +Fabric的智能合约称为Chaincode(链码),它处理网络成员所同意的业务逻辑。Fabric链码和底层账本是分开的,升级链码时并不需要迁移账本数据到新链码当中,实现了逻辑与数据的分离。 + +应用程序提交到区块链的Transaction,只能通过Chaincode执行,才能实现区块链的业务逻辑。只有Chaincode才能更新账本数据,其他模块都不能直接修改账本数据。 + +Chaincode可采用Go、Java、Node.js等语言编写。Chaincode被编译成一个独立的应用程序,然后这个Chaincode应用程序可以部署到Docker容器中运行。 + + +#### 二、底层角度 + +##### Membership Service(成员管理服务) +MSP(Membership Service Provider)对成员管理进行了抽象,对应用层的身份管理提供服务。每个MSP都会建立根信任证书体系,验证用户提交请求的签名。通过公钥基础设施PKI和去中心化共识机制使得非许可的区块链变成许可制的区块链。 + +用户获取证书需要通过CA系统,可以使用第三方的CA认证系统,也可以使用Fabric-CA服务。CA系统负责用户注册,并管理用户身份证书,比如新增或者撤销。注册的证书又分为三种类型,注册证书(ECert)用于用户身份,交易证书(TCert)用于交易签名,TLS证书(TLS Cert)用于TLS数据传输。 + + +##### Consensus Service(共识服务) +共识服务是区块链的核心组件,需要确保区块里面每一个交易数据的有效性和有序性,网络上不同节点之间数据的一致性。 + +共识机制由如下几个阶段组成:客户端向背书节点提交交易提案进行背书签名,背书节点把经过背书签名后的交易发送回客户端,客户端收集到足够数量的背书交易后把这些交易广播给排序服务节点,排序服务对交易进行排序并且产生区块(Block),之后把这些区块广播给记账节点,记账节点在验证所有的交易和背书信息都有效之后把区块(Block)写入账本。 + + +##### Chaincode Service(链码服务) +应用层的智能合约执行,需要依赖底层链码服务的实现。链码服务为智能合约提供安全的执行环境,确保执行过程的安全和用户数据的隔离,保证用户数据的私密性。Fabric采用Docker容器来管理和执行链码,提供安全的沙箱环境和镜像文件仓库。这有利于支持多种语言编写的链码,扩展性很好。 + +##### Security and Crypto Service(安全和加密服务) +安全问题是区块链的核心问题之一,对于企业级的联盟链更是重中之重。Fabric定义了一套BCCSP(Blockchain Cryptographic Service Provider)接口,可以根据这个接口来替换安全模块。在实现模块中,包含了密钥生成,Hash计算,签名以及验证签名,加密解密等等。 + + +-------------------------- +## 网络拓扑 + +如下图所示,Fabric 把运行时的网络切分成了多个不同的节点,节点是区块链上通信的主体,是一个逻辑概念。不同的节点可以运行在同一台物理机器上,也可以分散运行在不同的物理机器上。节点之间通过Gossip协议进行通信。节点类型包括:客户端、Peer节点、排序服务节点和CA节点 + +![](./images/fabric.topology.jpg) + +##### 客户端 + +客户端或者应用程序,代表了由最终用户操作的实体。它必须连接到某一个peer节点或者排序服务节点,才能与区块链网络通信。Fabric对于client端提供了SDK,让开发人员可以更容易地对接到区块链内的交易环节,交易的发起便是通过SDK进行。所有的通信信息都需要包含自己的签名,签名证书通过CA节点获得。客户端向Endorser节点提交交易提案并且从Endorser节点获取经过背书的交易,收集到足够多的交易之后,就广播给排序服务节点。 + +##### Peer节点 +Peer节点是参与交易的主体,可以说是代表了每个参与到链上的成员,负责共识环节中的执行智能合约。包含两种类型的节点,背书节点(Endorser)和记账节点(Committer),peer节点还存储了账本数据以及链码。 + +背书节点在接收到客户端的交易提案(Transaction Proposal)后,会验证交易签名,模拟执行交易,并且对结果执行签名背书,然后把经过背书的交易发送回客户端。背书节点是动态的角色,并且与具体的链码绑定,每个链码在实例化的时候都会设置背书策略,指定有效的交易背书需要由哪些节点完成。只有客户端向peer节点发起交易背书请求的时候,peer节点才是背书节点,其他时候只是一个普通的记账节点。只负责验证交易并且记账。 + +记账节点负责维护状态数据和账本。接收从排序服务发送过来的区块,首先会验证区块里面所有交易的有效性,还会验证所有的交易背书是否符合背书策略,最后把区块写入账本,更新状态数据库。 + +Peer节点有一种角色是主节点(Leader Peer),负责和排序服务通信,从排序服务节点处获得最新的区块,并且同步给组织内的其他peer节点。主节点可以提前设置好,也可以在运行过程中由所有的peer节点动态选举产生。 + +peer节点的配置是非常灵活的,可以是一个单独的记账节点,可以同时是背书节点和记账节点,还可以同时背书节点、记账节点和主节点。 + + +##### 排序服务节点 +排序服务节点(Orderer),主体功能便是对交易排序从而保证各Peer节点上的数据一致性,也包含了ACL进行访问控制。Orderer接收来自客户端的经过背书签名的交易,对还没有打包的交易进行排序并且打包生成区块,然后广播给peer节点。排序服务节点提供的是原子广播(Atomic Broadcast),保证同一个链上的节点接收到的消息都是相同的,而且在顺序上也是一致的。 + +排序服务中提供了多通道(Multi-Channel)能力,多通道实现了数据的隔离,保证了只有在同一个通道内的peer节点才能访问通道内的数据,有效的保护的数据的隐私性。 + +目前Fabric使用了Kafka来对交易进行排序。Orderer节点在网络中起到代理作用,多个Orderer节点会连接到Kafka集群,利用Kafka的排序功能,完成对网络中交易的排序和打包成区块的工作。对于每个通道,Orderer节点将其映射到Kafka集群中的一个topic(topic名称与channelID相同)上。Orderer目前并没有使用Kafka Topic的多分区负载均衡特性,默认每个topic只创建了一个分区(0号分区)。 + +* 客户端通过gRPC连接发送交易信息到Orderer节点的Broadcast()接口。 +* Orderer节点收到请求后,提取消息进行解析、检查,通过检查后封装为Kafka消息,通过Produce接口发送到Kakfa集群对应的topic分区中。当前消息数达到BatchSize.MaxMessageCount或消息尺寸过大,或超时时间达到BatchTimeout,则发送分块消息TTC-X到Kafka。 +* Kafka集群维护多个topic分区。Kakfa通过共识算法来确保写入到分区后的消息的一致性。即一旦写入分区,任何Orderer节点看到的都是相同的消息队列。 +* Orderer节点在启动后,还默认对本地账本对应的Kafka分区数据进行监听,不断从Kafka拉取(Consume)新的交易消息,并对消息进行处理。满足一定策略情况下(收到TTX-C或配置消息)还会将消息打包为区块。 + +##### CA节点 +CA节点负责对加入链内的所有节点进行授权认证,包括上层的client端,每一个节点都有其颁发的证书用于交易流程中的身份识别。在上图中,Membership部分就代表了CA节点的功能。 + +对于一个隐私性要求较高的区块链网络,节点需要授权才可以加入区块链网络,并且节点可能存在不同角色,CA从中起到授权用户、签名传输数据的作用。由于CA在授权区块链网络中的重要作用,Fabric将CA作为一个模块独立出来,可以独立运行。使用中需要根据区块链网络的特性,使用CA签发的数字证书进行认证、加密传输数据、授权用户、管理用户证书等。 + +下图描述了CA在Fabric中的工作方式 + +![](./images/fabric.ca.png) + +从图中可以看出,CA 服务器结构为树形结构,整个树形结构的根节点为根CA(Root Server),存在多个中间CA(Intermediate CA),图中每个中间CA服务器上可以配置一个CA服务集群,CA服务集群通过前置的HAproxy实现负载均衡。 + +Fabric CA提供了两种访问方式调用Server服务,一种是通过Client调用,另一种是通过SDK调用。两种调用都是REST风格的。 + +-------------------------- +## 账本 + +如下图所示,Fabric的账本数据结构包括以下几个部分: +* 区块链数据(Blockchain Data) +* 状态数据(State Database) +* 索引数据(Index Database) + +![](./images/fabric.ledger.png) + +##### 区块链数据(Blockchain Data) +区块链数据,又称为账本数据,就是我们通常所说的由一个一个的区块(Block)连接而成的链式数据结构。每一个区块中都存储有一条或一组有序的且不可篡改的记录。每个通道(channel)都有且仅有一个账本,在该通道(channel)中的每个加盟成员的对等点都维护同一份账本。 + +每一个区块包含三部分数据,区块头、交易数据和元数据。每一个区块头中又包含一个指向前一个区块头的Hash值,由此组成了一个完整的链(Chain),这部分数据是直接保存在文件系统里面的。对于这个完整的链式数据结构,已经产生的数据是无法修改的,只能查询,当有新的区块生成时,就附加在链的末尾,从而这个链逐渐增长,最终形成一个完整的交易记录。 + +所有针对数据状态变更的请求都会生成有序且不可篡改的记录保存于区块链数据中。数据状态的变更是由所有参与方认可的智能合约调用事务的结果。每个事务都将产生一组Key/Value对,这些键值对作为创建、更新或删除等操作而同步到所有账本。 + + +##### 状态数据(State Database) +状态数据,也称为世界状态(World State),它不是链式数据结构的一部分,而是链式数据结构综合而成的一个结果。这部分数据是保存在Level DB数据库中的,是可以修改的。状态数据只是保存所有交易完成之后的最新结果,全部都是以Key/Value数据对的形式存在,并不会保存交易过程。当有新的交易产生的时候,peer节点会根据最新的交易信息修改状态数据库,以反映出交易的结果。当需要详细的交易记录的时候,就去区块链数据里面查询。 + + +##### 索引数据 +索引数据库是为了查询区块链数据的时候,可以快速定位,加快查询速度。在索引数据中,保存了两类数据,一类是对于每一个Key的历史记录的追踪信息,另一类是对于block的相关信息,包括区块hash,区块号,交易号等等。 + +-------------------------- +## 多通道(Multi Channel) + +通道是两个或多个特定网络成员之间的通信的私有“子网”,用于进行需要数据保密的交易。Channel由成员(组织)、每个成员的锚点、共享账本、链码应用程序和Order服务节点定义。网络上的每个Transaction都在一个Channel上执行,每个通信方必须经过身份验证并授权在该Channel上进行交易。加入Channel的每个peer都具有由成员服务提供商(MSP)给出的自己的身份。 + +要创建新的channel,客户端SDK会调用configuration system chaincode和引用属性,如锚点和成员(组织)。该请求为channel ledger创建一个genesis block,它存储有关channel的策略,成员和锚点的配置信息。当将新成员添加到现有channel时,这个genesis block或最近被重新配置的块将会分享给新成员。(具体可参阅configtx部分) + +Channel中每个成员的Leading Peer的选举决定了哪个peer代表成员与Ordering Service进行通信。如果没有指定Leader,则可以使用算法来指定Leader。共识服务将交易排序并以一个Block的形式发送给一个Leader,然后Leader将其分发给其成员peer。 + +虽然任何一个锚点可以属于多个通道,并且因此维护多个账本,但没有账本数据可以从一个channel传递到另一个channel。账本按channel分隔,由configuration chaincode,identity membership service和gossip数据传播协议来定义和实现。被隔离的数据包括交易信息,账本状态和channel成员资料,这些数据仅限于在channel上具有可验证成员资格的peer间传播。通过信道隔离peer和账本数据,允许需要私有和机密事务的网络成员与同一个块链网络上的业务竞争对手和其他受限制的成员共存。 + +![](./images/fabric.multi.channel.png) + +在上图中,一共有两个通道, + +通道1(红色实线): +包含的节点有:1.1,1.2,2.1,2.2 Leader节点:1.1,2.1 + +通道2(蓝色实线): +包含的节点有:1.1,1.3,2.2,2.3,3.1,3.2,3.3 + +每一个通道,都是一个独立的区块链,因此上图包含两个链。 + +-------------------------- +### 子账本 + +通道提供了一种通讯机制,将peer和orderer连接在一起,形成一个个具有保密性的通讯链路(虚拟)。在每一个通道中的数据都是与其他通道隔离的,并且数据会单独保存,由此形成了一个一个的子账本。从本质上来说,就是每一个通道都有一个独立的账本数据。账本可以被创建,并绑定到一个通道 + +![](./images/fabric.sub.ledger.png) + +在上图中,一个包含了三个通道,也就包含了三个独立的子账本 + +通道1(红色): +包含的节点有:Peer 1,Peer 2,Peer N + +通道2(黑色): +包含的节点有:Peer 2,Peer N + +通道3(蓝色): +包含的节点有:Peer 1,Peer N + +-------------------------- +## 交易流程 + +下图所示为一个典型的Fabric交易流程 + +![](./images/fabric.transaction.flow.png) + + +##### 1. 客户端构造交易提案(Transaction Proposal) + +客户端应用程序利用任意SDK(Node.js,java,python)构造交易提案proposal。该提案是一个调用智能合约功能函数的请求,用来确认哪些数据可以读取或写入账本。交易提案中包含本次交易要调用的合约标识、合约方法和参数信息以及客户端签名等。SDK将交易提案打包为可识别的格式(如gRPC上的protocol buffer),并使用用户的加密凭证为该交易提案生成唯一的签名。客户端把交易提案发送给一个或多个背书节点。 + + +##### 2. 背书节点模拟执行交易 + +背书节点(Endorser)收到交易提案后,验证签名并确定提交者是否有权执行操作。背书节点将交易提案的参数作为输入,在当前状态KV数据库上执行交易,生成包含执行返回值、读操作集合和写操作集合的交易结果(此时不会更新账本),这些值的集合、背书节点的签名和背书结果(YES/NO)作为提案的结果返回给客户端SDK,SDK解析这些信息判断是否应用于后续的交易。 + + +##### 3. 客户端把交易发送到排序服务(Order Service) + +应用程序使用SDK验证背书节点签名,并比较各节点返回的提案结果,判断提案结果是否一致以及是否参照指定的背书策略执行。客户端收到各个背书节点的应答后,打包到一起组成一个交易并签名,发送给Orderers。 + +##### 4. 共识排序,生成新区块 + +Orderers对接收到的交易进行排序,然后按照区块生成策略,将一批交易打包到一起,生成新的区块,调用deliver API投递消息,发送给提交节点。 + +##### 5. 更新账本 +提交节点(Committer)收到区块后,会对区块中的每笔交易进行校验,检查交易依赖的输入输出是否符合当前区块链的状态,然后还要验证交易的背书是否符合相应的背书策略,所有的验证都通过之后,将区块追加到本地的区块链,并修改KV状态数据库。 diff --git "a/fabric/fabric \346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\344\277\235\350\257\201.md" "b/fabric/fabric \346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\344\277\235\350\257\201.md" new file mode 100644 index 0000000..c940a7f --- /dev/null +++ "b/fabric/fabric \346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\344\277\235\350\257\201.md" @@ -0,0 +1,17 @@ +## fabric 数据一致性保证 + + +#### 存储数据结构 +* 存储数据结构,区块链大部分数据结构都是每个存储单元包含上一存储单元的hash值,把所有数据连接在一起用来保障数据的前后依赖达到可追溯的特点 + +![](./images/fabric.consistency.block) + +#### 网络协议 + +* Gossip是一个带冗余的容错算法,更进一步,Gossip是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在”最终“所有节点一致。因为图中如果同一个channel的peer很多的话,单点leader负载会很高,所以这里面采用分散排序节点和leader的压力来用gossip保证数据的最终一致性 +![](./images/fabric.consistency.gossip) + +* fabric是中心化的架构,各个org节点选举出leader订阅order节点的消息然后leader再将消息散播给组内其他成员。也就是说整个组织是完全信任leader订阅的消息,如果这里作恶是没有办法防范的,这个是因为联盟链首先是建立在节点都是可信赖的基础上而不像公链建立在不可信的基础上 +* 其他节点如果恶意篡改数据,block文件有hash链支撑,如果有暴力编辑修改,事后可以检查出不一致,VSCC会验证交易读操作与状态匹配,peer node一重新启动,会重构stateDb +* peer定期pull from 其他相邻peer,比较差异,如发现本地block-seqno小于其他,则发StateRequestMsg广播请求差异块,放本地缓冲 --> 排序,等待连续后,连续追加到链尾。由于是个相对封闭的网络,所以冗余校验浪费的成本可以忽略不计。 + diff --git a/fabric/images/fabric.block.data.structure.png b/fabric/images/fabric.block.data.structure.png new file mode 100644 index 0000000..ced4893 Binary files /dev/null and b/fabric/images/fabric.block.data.structure.png differ diff --git a/fabric/images/fabric.ca.png b/fabric/images/fabric.ca.png new file mode 100644 index 0000000..b67da89 Binary files /dev/null and b/fabric/images/fabric.ca.png differ diff --git a/fabric/images/fabric.consistency.block b/fabric/images/fabric.consistency.block new file mode 100644 index 0000000..50c1d7d Binary files /dev/null and b/fabric/images/fabric.consistency.block differ diff --git a/fabric/images/fabric.consistency.gossip b/fabric/images/fabric.consistency.gossip new file mode 100644 index 0000000..66b57f0 Binary files /dev/null and b/fabric/images/fabric.consistency.gossip differ diff --git a/fabric/images/fabric.ledger.png b/fabric/images/fabric.ledger.png new file mode 100644 index 0000000..e782a46 Binary files /dev/null and b/fabric/images/fabric.ledger.png differ diff --git a/fabric/images/fabric.logic.architecture.jpg b/fabric/images/fabric.logic.architecture.jpg new file mode 100644 index 0000000..72e3fb7 Binary files /dev/null and b/fabric/images/fabric.logic.architecture.jpg differ diff --git a/fabric/images/fabric.multi.channel.png b/fabric/images/fabric.multi.channel.png new file mode 100644 index 0000000..cba043a Binary files /dev/null and b/fabric/images/fabric.multi.channel.png differ diff --git a/fabric/images/fabric.sub.ledger.png b/fabric/images/fabric.sub.ledger.png new file mode 100644 index 0000000..22d5e31 Binary files /dev/null and b/fabric/images/fabric.sub.ledger.png differ diff --git a/fabric/images/fabric.topology.jpg b/fabric/images/fabric.topology.jpg new file mode 100644 index 0000000..fc19837 Binary files /dev/null and b/fabric/images/fabric.topology.jpg differ diff --git a/fabric/images/fabric.transaction.flow.png b/fabric/images/fabric.transaction.flow.png new file mode 100644 index 0000000..93d32f1 Binary files /dev/null and b/fabric/images/fabric.transaction.flow.png differ diff --git a/khipu/img/main-sdl.png b/khipu/img/main-sdl.png new file mode 100644 index 0000000..087c8fd Binary files /dev/null and b/khipu/img/main-sdl.png differ diff --git a/khipu/img/parallel-sdl.png b/khipu/img/parallel-sdl.png new file mode 100644 index 0000000..dfcc3e6 Binary files /dev/null and b/khipu/img/parallel-sdl.png differ diff --git a/khipu/img/parallel.png b/khipu/img/parallel.png new file mode 100644 index 0000000..cdc4778 Binary files /dev/null and b/khipu/img/parallel.png differ diff --git "a/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" new file mode 100644 index 0000000..67c88c9 --- /dev/null +++ "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\200\346\246\202\350\277\260.md" @@ -0,0 +1,64 @@ +# khipu并行分析之一概述 + +## 一、介绍 +khipu是用scala编写的一个初步的以太坊的优化产品,据其本人宣称速度还是有较大提高的,提高速度的原因是采用了并行交易,为了和星云链的并行交易进行对比,这里首先分析一下其并行的特点和效果。 +
+ +## 二、主要特点 + +交易并行的限制在于三点: +
+1、相同帐户(地址)的原子控制。 +
+2、相同帐户(地址)的数据stroage的并发。 +
+3、相同帐户(地址)的EVM的OPCODE并发。 +
+khipu解决的方法是进行的交易都从前一期区块的世界状态开始,并行运行,并在运行过程记录这三种竞态的状态。然后执行完成后,开始合并。合并过程对冲突进行判断,没有直接合并,反之则放到已经合并的世界状态于执行一次,然后再次合并世界状态 。使用区块哈希进行最后校验。如果有误,则放弃前面的并行方案,回退到原始位置串行交易。 +
+其引入一个并行指标,来计算并行结果。其实际测试的结果是有百分之八十可以进行交行。根据安达尔定理,理想状态下,这样做可以五倍提高速度。 +
+ +## 三、类图和流程 + +## 1、类图 +主要的类图如下: +
+ +![uml-class](img/parallel.png) +
+从上图可以看出,重点在 +
+ +## 2、流程图 +基本的流程图如下: +
+ +![sdl](img/main-sdl.png) +
+基本的并行的流程图如下: +
+ +![sdl0](img/parallel-sdl.png) +
+从流程图中可以分析出,真正的并行,其实是在执行交易验证时,使用了future对map映射,通过TxProcessor来进行同时操作。 +
+ +## 四、并行主体 +在源码的分析中,基本可以看到以下几点: +
+1、在Ledger.scala文件中操作并行的主流程。包括启动验证,发起并行交易,初始状态的载入和最后状态的合并。 +
+2、在VM中通过对指令的切分利用OpenCode来进行状态的更新。 +
+3、通过WorldState(BlockWroldstate)等实现状态的保存和修改。 +
+4、利用TxProcessor来完成单个交易的执行,换句话说,在1中的并行交易,最终要拆解到这里来执行。 +
+5、并行交易只执行两次,如果未成功转成串行。 +
+ +## 五、总结 +通过对khipu的初步分析,在整体上看来,实现的步骤简单有效,当然,在内网中测试是什么情况,能不能达到和公网一样的效果,这个得看最后的实际测试情况。但是解决问题的方法还是值得借鉴的。 +
+> diff --git "a/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\211\345\271\266\350\241\214\345\244\204\347\220\206\345\222\214\351\252\214\350\257\201.md" "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\211\345\271\266\350\241\214\345\244\204\347\220\206\345\222\214\351\252\214\350\257\201.md" new file mode 100644 index 0000000..5164d84 --- /dev/null +++ "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\270\211\345\271\266\350\241\214\345\244\204\347\220\206\345\222\214\351\252\214\350\257\201.md" @@ -0,0 +1,547 @@ +# khipu并行分析之三并行处理和验证 + +## 一、并行的启动 +并行的处理从两个阶段展开,一个是自己挖矿成功时对交易的并行处理(这里需要注意的是准备块时的预执行是串行的);另外一个就是在接收到其它节点传播过来的块时进行的并行交易处理。在前面提到过,在khipu中是并行交易分别存储状态,然后在合并状态时串行操作。这样的好处是:增加了并行的速度,不使用锁安全性会有所增强,而且状态的合并本身并不是耗时的重点,所以基本上不会形成瓶颈。 +
+优点明显,缺点也比较明显,所有的验证放到最后,那么如果交易冲突较多时,反而会引起性能的下降,不过针对公网的交易,冲突项还是相对较小的,以他们自己的测试来看,应该有三到五倍的提高。按照其文档说明,优势最容易体现出来的是可以使用机械硬盘进行全节点的同步。 +
+这里不是分析网络同步,所以会直接深入到块的并行交易处进行分析。 + +## 二、并行的处理 + +## 1、并行的执行 +挖矿开始: +
+ +``` +// TODO improve mined block handling - add info that block was not included because of syncing [EC-250] +// we allow inclusion of mined block only if we are not syncing / reorganising chain +private def processMinedBlock(block: Block) { + if (workingHeaders.isEmpty && !isRequesting) { + // we are at the top of chain we can insert new block + blockchain.getTotalDifficultyByHash(block.header.parentHash) match { + case Some(parentTd) if block.header.number > appStateStorage.getBestBlockNumber => + // just insert block and let resolve it with regular download + //此处开始并行调用 + val f = executeAndInsertBlock(block, parentTd, isBatch = false) andThen { + case Success(Right(newBlock)) => + // broadcast new block + handshakedPeers foreach { + case (peerId, (peer, peerInfo)) => peer.entity ! PeerEntity.MessageToPeer(peerId, newBlock) + } + + case Success(Left(error)) => + + case Failure(e) => + } + Await.result(f, Duration.Inf) + case _ => + log.error("Failed to add mined block") + } + } else { + ommersPool ! OmmersPool.AddOmmers(List(block.header)) + } +} +``` +
+接收其它区块开始: +
+ +``` +//doProcessBlockBodies同样会调用 +private def executeAndInsertBlocks(blocks: Vector[Block], parentTd: UInt256, isBatch: Boolean): Future[(UInt256, Vector[NewBlock], Vector[BlockExecutionError])] = { + blocks.foldLeft(Future.successful(parentTd, Vector[NewBlock](), Vector[BlockExecutionError]())) { + case (prevFuture, block) => + prevFuture flatMap { + case (parentTotalDifficulty, newBlocks, Vector()) => + //此处与挖矿一样调用同一函数,注意二者不同 + executeAndInsertBlock(block, parentTotalDifficulty, isBatch) map { + case Right(newBlock) => + // reset lookbackFromBlock only when executeAndInsertBlock success + lookbackFromBlock = None + + // check blockHashToDelete + blockchain.getBlockHeaderByNumber(block.header.number).map(_.hash).filter(_ != block.header.hash) foreach blockchain.removeBlock + + (newBlock.totalDifficulty, newBlocks :+ newBlock, Vector()) + case Left(error) => + (parentTotalDifficulty, newBlocks, Vector(error)) + } + + case (parentTotalDifficulty, newBlocks, errors) => + Future.failed(ExecuteAndInsertBlocksAborted(parentTotalDifficulty, newBlocks, errors)) + } + } recover { + case ExecuteAndInsertBlocksAborted(parentTotalDifficulty, newBlocks, errors) => + (parentTotalDifficulty, newBlocks, errors) + } +} +``` +
+它们最终会调用execBlock中的executeBlockTransactions这个函数: +
+ +``` +override def executeBlock(block: Block, validators: Validators)(implicit executor: ExecutionContext): Future[Either[BlockExecutionError, BlockResult]] = { + val start1 = System.nanoTime + //启动并行 + val parallelResult = executeBlockTransactions(block, validators.signedTransactionValidator, isParallel = true && !blockchainConfig.isDebugTraceEnabled) map { + case Right(blockResult) => + ...... + case Left(error) => Left(error) + } + + //处理并行结果,如果验证不通过,改做串行 + parallelResult flatMap { + case Right((blockResult, worldCommitted)) => Future.successful(Right(blockResult)) + + case left @ Left(error) => + log.debug(s"in parallel failed with error $error, try sequential ...") + + val start1 = System.nanoTime + //重点在这里,最后一个布尔值变成了false,意味着串行 + executeBlockTransactions(block, validators.signedTransactionValidator, isParallel = false) map { + case Right(blockResult) => + ...... + } + + case Left(error) => Left(error) + } + } + } +private def executeBlockTransactions( + block: Block, + stxValidator: SignedTransactionValidator, + isParallel: Boolean +)(implicit executor: ExecutionContext): Future[Either[BlockExecutionError, BlockResult]] = { + val parentStateRoot = blockchain.getBlockHeaderByHash(block.header.parentHash).map(_.stateRoot) + val evmCfg = EvmConfig.forBlock(block.header.number, blockchainConfig) + + def initialWorld = blockchain.getWorldState(block.header.number, blockchainConfig.accountStartNonce, parentStateRoot) + +//在这里判断配置的是串行还是并行交易,并根据配置的数量进行并行 + if (isParallel) { + executeTransactions_inparallel(block.body.transactionList, block.header, stxValidator, evmCfg)(initialWorld) + } else { + executeTransactions_sequential(block.body.transactionList, block.header, stxValidator, evmCfg)(initialWorld) + } +} +``` +
+下面是真正的并行函数: +
+ +``` +private def executeTransactions_inparallel( + signedTransactions: Seq[SignedTransaction], + blockHeader: BlockHeader, + stxValidator: SignedTransactionValidator, + evmCfg: EvmConfig +)(initialWorldFun: => BlockWorldState)(implicit executor: ExecutionContext): Future[Either[BlockExecutionError, BlockResult]] = { + val nTx = signedTransactions.size + //类似于这种计时统计可以忽略 + val start = System.nanoTime + blockchain.storages.accountNodeDataSource.clock.start() + blockchain.storages.storageNodeDataSource.clock.start() + blockchain.storages.evmCodeDataSource.clock.start() + blockchain.storages.blockHeaderDataSource.clock.start() + blockchain.storages.blockBodyDataSource.clock.start() + + //并行从这里开始,形成future,并交给TxProcessor去执行,在每个TxProcessor中有一个Work,来进行交易的执行 + val fs = signedTransactions.map(stx => stx -> initialWorldFun.withTx(Some(stx))) map { + case (stx, initialWorld) => + (txProcessor ? TxProcessor.ExecuteWork(initialWorld, stx, blockHeader, stxValidator, evmCfg))(txProcessTimeout).mapTo[(Either[BlockExecutionError, TxResult], Long)] // recover { case ex => s"$ex.getMessage" } + } + + //并行执行 + Future.sequence(fs) map { rs => + val dsGetElapsed1 = blockchain.storages.accountNodeDataSource.clock.elasped + blockchain.storages.storageNodeDataSource.clock.elasped + + blockchain.storages.evmCodeDataSource.clock.elasped + blockchain.storages.blockHeaderDataSource.clock.elasped + blockchain.storages.blockBodyDataSource.clock.elasped + + val cacheHitRates = List(blockchain.storages.accountNodeDataSource.cacheHitRate, blockchain.storages.storageNodeDataSource.cacheHitRate).map(_ * 100.0) + + //忽略 + blockchain.storages.accountNodeDataSource.clock.start() + blockchain.storages.storageNodeDataSource.clock.start() + blockchain.storages.evmCodeDataSource.clock.start() + blockchain.storages.blockHeaderDataSource.clock.start() + blockchain.storages.blockBodyDataSource.clock.start() + + val (results, elapses) = rs.unzip + val elapsed = elapses.sum + log.debug(s"${blockHeader.number} executed parallel in ${(System.nanoTime - start) / 1000000}ms, db get ${100.0 * dsGetElapsed1 / elapsed}%") + + var currWorld: Option[BlockWorldState] = None + var txError: Option[BlockExecutionError] = None + var txResults = Vector[TxResult]() + var parallelCount = 0 + + // re-execute tx under prevWorld, commit prevWorld to get all nodes exist, see BlockWorldState.getStorage and getStateRoott + var reExecutedElapsed = 0L + //定义一个重新执行的函数,有点类似c++的lambda + def reExecute(stx: SignedTransaction, prevWorld: BlockWorldState) = { + var start = System.nanoTime + log.debug(s"${stx.hash} re-executing") + // should commit prevWorld's state, since we may need to get newest account/storage/code by new state's hash + //保存世界状态和结果 + validateAndExecuteTransaction(stx, blockHeader, stxValidator, evmCfg)(prevWorld.commit().withTx(Some(stx))) match { + case Left(error) => txError = Some(error) + case Right(newTxResult) => + currWorld = Some(newTxResult.world) + txResults = txResults :+ newTxResult + } + reExecutedElapsed += System.nanoTime - start + } + + val itr = results.iterator + while (itr.hasNext && txError.isEmpty) { + val r = itr.next() + r match { + case Right(txResult) => + currWorld match { + case None => // first tx + parallelCount += 1 + currWorld = Some(txResult.world) + txResults = txResults :+ txResult + + case Some(prevWorld) => + if (txResult.parallelRaceConditions.nonEmpty) { + log.debug(s"tx ${txResult.stx.hash} potential parallel race conditions ${txResult.parallelRaceConditions} occurred during executing") + // when potential parallel race conditions occurred during executing, it's difficult to judge if it was caused by conflict, so, just re-execute + reExecute(txResult.stx, prevWorld) + } else { + //合并世界状态 + prevWorld.merge(txResult.world) match { + case Left(raceCondiftions) => + log.debug(s"tx ${txResult.stx.hash} has race conditions with prev world state:\n$raceCondiftions") + //再次执行 + reExecute(txResult.stx, prevWorld) + + case Right(mergedWorld) => + parallelCount += 1 + currWorld = Some(mergedWorld) + txResults = txResults :+ txResult + } + } + } + + case Left(error @ TxsExecutionError(_, stx, _, SignedTransactionError.TransactionSenderCantPayUpfrontCostError(_, _))) => + currWorld match { + case None => txError = Some(error) // first tx + case Some(prevWorld) => + reExecute(stx, prevWorld) + } + + case Left(error) => txError = Some(error) + } + + //log.debug(s"${blockHeader.number} touched accounts (${r.fold(_.stx, _.stx).hash}):\n ${currWorld.map(_.touchedAccounts.mkString("\n", "\n", "\n")).getOrElse("")}") + } + + val dsGetElapsed2 = blockchain.storages.accountNodeDataSource.clock.elasped + blockchain.storages.storageNodeDataSource.clock.elasped + + blockchain.storages.evmCodeDataSource.clock.elasped + blockchain.storages.blockHeaderDataSource.clock.elasped + blockchain.storages.blockBodyDataSource.clock.elasped + + //忽略并行参数统计的计算 + val parallelRate = if (parallelCount > 0) { + parallelCount * 100.0 / nTx + } else { + 0.0 + } + val dbReadTimePerc = 100.0 * (dsGetElapsed1 + dsGetElapsed2) / (elapsed + reExecutedElapsed) + + log.debug(s"${blockHeader.number} re-executed in ${reExecutedElapsed}ms, ${100 - parallelRate}% with race conditions, db get ${100.0 * dsGetElapsed2 / reExecutedElapsed}%") + log.debug(s"${blockHeader.number} touched accounts:\n ${currWorld.map(_.touchedAccounts.mkString("\n", "\n", "\n")).getOrElse("")}") + + txError match { + case Some(error) => Left(error) + case None => postExecuteTransactions(blockHeader, evmCfg, txResults, Stats(parallelCount, dbReadTimePerc, cacheHitRates))(currWorld.map(_.withTx(None)).getOrElse(initialWorldFun)) + } + } andThen { + case Success(_) => + case Failure(e) => log.error(e, s"Error on block ${blockHeader.number}: ${e.getMessage}") + } +} +``` +这样一个并行的处理就开始了。 +
+ +## 2、并行的状态保存 +在上面开始并行交易后,会发现,真正用来处理并行的是调用validateAndExecuteTransaction这个函数,调用这个函数的地方有两处,一处是前面提到的TxProcessor中,另外一处在reExecute中。因为后者其实是处理错误异常的情况,所以直接看前面的就可以,它会调用executeTransaction,同时会计算一下费用。 +
+ +``` +private def executeTransaction( + stx: SignedTransaction, + blockHeader: BlockHeader, + evmCfg: EvmConfig +)(world: BlockWorldState): TxResult = { + val start = System.nanoTime + + // TODO catch prepareProgramContext's throwable (MPTException etc from mtp) here + val (checkpoint, context) = prepareProgramContext(stx, blockHeader, evmCfg)(world) + + if (blockchainConfig.isDebugTraceEnabled) { + println(s"\nTx 0x${stx.hash} ========>") + } + + //重点这里,会调用虚拟机,将交易执行 + val result = runVM(stx, context, evmCfg)(checkpoint) + + val gasLimit = stx.tx.gasLimit + val totalGasToRefund = calcTotalGasToRefund(gasLimit, result) + val gasUsed = stx.tx.gasLimit - totalGasToRefund + val gasPrice = stx.tx.gasPrice + val txFee = gasPrice * gasUsed + val refund = gasPrice * totalGasToRefund + + if (blockchainConfig.isDebugTraceEnabled) { + println(s"\nTx 0x${stx.hash} gasLimit: ${stx.tx.gasLimit} gasUsed $gasUsed, isRevert: ${result.isRevert}, error: ${result.error}") + } + + val worldRefundGasPaid = result.world.pay(stx.sender, refund) + val worldDeletedAccounts = deleteAccounts(result.addressesToDelete)(worldRefundGasPaid) + + val elapsed = System.nanoTime - start + TxResult(stx, worldDeletedAccounts, gasUsed, txFee, result.txLogs, result.addressesTouched, result.returnData, result.error, result.isRevert, result.parallelRaceConditions) +} +private def runVM(stx: SignedTransaction, context: PC, evmCfg: EvmConfig)(checkpoint: BlockWorldState): PR = { + val r = if (stx.tx.isContractCreation) { // create + //真正的虚拟机执行 + VM.run(context, blockchainConfig.isDebugTraceEnabled) + } else { // call + //类似于以太坊的固有的合约 + PrecompiledContracts.getContractForAddress(context.targetAddress, evmCfg) match { + case Some(contract) => + contract.run(context)//此处也会调用虚拟机相关的OPCODE + case None => + VM.run(context, blockchainConfig.isDebugTraceEnabled) + } + } + + //处理智能合约 + val result = if (stx.tx.isContractCreation && !r.error.isDefined && !r.isRevert) { + saveCreatedContract(context.env.ownerAddr, r, evmCfg) + } else { + r + } + + if (result.error.isDefined || result.isRevert) { + // rollback to the world before transfer was done if an error happened + // the error result may be caused by parallel conflict, so merge all possible modifies + //合并竞态条件 + result.copy(world = checkpoint.mergeRaceConditions(result.world), addressesToDelete = Set(), addressesTouched = Set(), txLogs = Vector(), parallelRaceConditions = Set(ProgramState.OnError)) + } else { + result + } +} +``` +
+通过上面的反复跳转进入VM: +
+ +``` +//首先要处理当前状态,从程序上下文中把状态取出来进行处理 +def run[W <: WorldState[W, S], S <: Storage[S]](context: ProgramContext[W, S], isDebugTraceEnabled: Boolean): ProgramResult[W, S] = { + // new init state is created for each run(context) + val initState = new ProgramState[W, S](context, isDebugTraceEnabled) + val postState = run(initState) + + ProgramResult[W, S]( + postState.returnData, + postState.gas, + postState.world, + postState.txLogs, + postState.gasRefund, + postState.addressesToDelete, + postState.addressesTouched, + postState.error, + postState.isRevert, + postState.parallelRaceConditions + ) + } + + // TODO write debug trace to a file + @tailrec + private def run[W <: WorldState[W, S], S <: Storage[S]](state: ProgramState[W, S]): ProgramState[W, S] = { + val byte = state.program.getByte(state.pc) + state.config.getOpCode(byte) match { + case Some(opcode) => + if (state.isDebugTraceEnabled) { + println(s"[trace] $opcode | pc: ${state.pc} | depth: ${state.env.callDepth} | gas: ${state.gas} | ${state.stack} | ${state.memory} | error: ${state.error}") + } + //此处开始进行指令级的处理 + val newState = opcode.execute(state) // may reentry VM.run(context) by CREATE/CALL op + + if (newState.isHalted) { + if (state.isDebugTraceEnabled) { + println(s"[trace] halt | pc: ${newState.pc} | depth: ${newState.env.callDepth} | gas: ${newState.gas} | ${newState.stack} | ${newState.memory} | error: ${newState.error}") + } + newState + } else { + run[W, S](newState) + } + + case None => + if (state.isDebugTraceEnabled) { + println(s"[trace] ${InvalidOpCode(byte)} | pc: ${state.pc} | depth: ${state.env.callDepth} | gas: ${state.gas} | ${state.stack} | error: ${state.error}") + } + state.withError(InvalidOpCode(byte)).halt() + } + } +``` +
+他会根据不同的指定来调用不同的处理函数exec,举一个例子在sstore这个指令中,会对世界状态进行存储: +
+ +``` +protected def exec[W <: WorldState[W, S], S <: Storage[S]](state: ProgramState[W, S], params: (UInt256, UInt256)): ProgramState[W, S] = { + if (state.context.isStaticCall) { + state.withError(StaticCallModification) + } else { + val (key, value) = params + val oldValue = state.storage.load(key) + val refund = if (value.isZero && oldValue.nonZero) state.config.feeSchedule.R_sclear else 0 + //保存状态 + val updatedStorage = state.storage.store(key, value) + val world = state.world.saveStorage(state.ownAddress, updatedStorage) + + state + .withWorld(world) + .refundGas(refund) + .step() + } + } +``` +
+其它的都类似,不再一一赘述。 +
+ +## 3、并行的再处理 +前面提到过,如果状态合并有问题,就会再来一次。看一下这个代码: +
+ +``` +def reExecute(stx: SignedTransaction, prevWorld: BlockWorldState) = { + //保存世界状态和结果 + validateAndExecuteTransaction(stx, blockHeader, stxValidator, evmCfg)(prevWorld.commit().withTx(Some(stx))) match { + } +} +``` +
+其实去除相关的处理后发现和第一执行没有啥区别,其实它的原理也就是说,如果第一次合并冲突,那么第二次再执行时,极有可能就已经没有冲突了。 +
+ +## 4、并行的验证 +
+ +``` +override def executeBlock(block: Block, validators: Validators)(implicit executor: ExecutionContext): Future[Either[BlockExecutionError, BlockResult]] = { + val start1 = System.nanoTime + ...... + // 根据返回结果来决定是成功还是串行再来一次 + parallelResult flatMap { + case Right((blockResult, worldCommitted)) => Future.successful(Right(blockResult)) + + case left @ Left(error) => + log.debug(s"in parallel failed with error $error, try sequential ...") + + val start1 = System.nanoTime + executeBlockTransactions(block, validators.signedTransactionValidator, isParallel = false) map { + case Right(blockResult) => + log.debug(s"${block.header.number} sequential-executed in ${(System.nanoTime - start1) / 1000000}ms") + + val worldRewardPaid = payBlockReward(block)(blockResult.world) + val worldCommitted = worldRewardPaid.commit() // State root hash needs to be up-to-date for validateBlockAfterExecution + + validateBlockAfterExecution(block, worldCommitted.stateRootHash, blockResult.receipts, blockResult.gasUsed, validators.blockValidator) match { + case Right(_) => Right(blockResult) + case Left(error) => Left(error) + } + + case Left(error) => Left(error) + } + } +} +``` +
+验证的代码比较简单,其实就对并行的结果进行一下处理即可。 +
+ +## 5、并行的状态合并 +状态合并其实是重中之重,前面所有的并行有没有意义,是由他们促成的,如果无法促成,就只能回到串行执行,那成本可就大了。程序会在前面的并行交易函数中调用prevWorld.merge(txResult.world),看一下这个函数: +
+ +``` +//合并竞态条件 +def mergeRaceConditions(later: BlockWorldState): BlockWorldState = { + later.raceConditions foreach { + case (k, vs) => this.raceConditions += (k -> (this.raceConditions.getOrElse(k, Set()) ++ vs)) + } + this + } +//调用此处的合并,它又会对世界状态中的竞态条件,用户trie等进行合并 + private[ledger] def merge(later: BlockWorldState): Either[Map[RaceCondition, Set[Address]], BlockWorldState] = { + val raceCondiftions = this.raceConditions.foldLeft(Map[RaceCondition, Set[Address]]()) { + case (acc, (OnAccount, addresses)) => acc + (OnAccount -> addresses.filter(later.trieAccounts.logs.contains)) + case (acc, (OnStorage, addresses)) => acc + (OnStorage -> addresses.filter(later.trieStorages.contains)) + case (acc, (OnCode, addresses)) => acc + (OnCode -> addresses.filter(later.codes.contains)) + case (acc, (OnAddress, addresses)) => acc + (OnAddress -> addresses.filter(x => later.codes.contains(x) || later.trieStorages.contains(x) || later.trieAccounts.logs.contains(x))) + } filter (_._2.nonEmpty) + + if (raceCondiftions.isEmpty) { + val toMerge = this.copy + toMerge.touchedAddresses ++= later.touchedAddresses + //mergeAccountTrieAccount_simple(toMerge, that) + toMerge.mergeAccountTrieAccount(later).mergeTrieStorage(later).mergeCode(later).mergeRaceConditions(later) + Right(toMerge) + } else { + Left(raceCondiftions) + } + } + /** mergeAccountTrieAccount should work now, mergeAccountTrieAccount_simple is left here for reference only */ + private def mergeAccountTrieAccount_simple(later: BlockWorldState): BlockWorldState = { + this.trieAccounts.logs ++= later.trieAccounts.logs + this + } +//合并用户的TrieAccount + private def mergeAccountTrieAccount(later: BlockWorldState): BlockWorldState = { + val alreadyMergedAddresses = later.accountDeltas map { + case (address, deltas) => + val valueMerged = deltas.foldLeft(this.getAccount(address).getOrElse(this.emptyAccount)) { + case (acc, AccountDelta(nonce, balance, _, _)) => acc.increaseNonce(nonce).increaseBalance(balance) + } + + // just put the lasted stateRoot and codeHash of y and merge delete + later.trieAccounts.logs.get(address).map { + case Updated(Account(_, _, stateRoot, codeHash)) => this.trieAccounts += (address -> valueMerged.withStateRoot(stateRoot).withCodeHash(codeHash)) + case Original(_) => this.trieAccounts += (address -> valueMerged) + case Deleted(_) => this.trieAccounts -= address + } + + address + } toSet + + this.trieAccounts.logs ++= (later.trieAccounts.logs -- alreadyMergedAddresses) + this + } + + private def mergeTrieStorage(later: BlockWorldState): BlockWorldState = { + this.trieStorages ++= later.trieStorages + this + } + + private def mergeCode(later: BlockWorldState): BlockWorldState = { + this.codes ++= later.codes + this + } +``` +
+通过上述的一系列的合并,能合并的就合并了不能合并的就返回错误。然后回到串行处理过程。 +
+ +## 三、总结 +不能不说khipu的并行思路有自己独特的一方面,正如他们的官方的文档上说,处理并行的速度决定于安达尔定理的失败的一方,也有就是说,只有是失败的倒数,从他们自己的官方宣布来看,其提供的数据支持在以太坊公网上应该会有比较明显的提升(并行度可以提高到80%,按照定理,理论上应该有五倍的提升)。 +
+从khipu总结的经验来看,计算性能的瓶颈有三点:网络广播时延;共识时间长和节点对交易(合约)执行、验证的时间。在时延和共识确定的情况下,提高第三者是一个比较容易达到的目标,但是也是一个比较麻烦的目标。 +
+从目前来看,包括星云、早期的EOS等都提供了交易并行,各有千秋,各有特色。最新的以太坊也有这个想法,但最终会实现成什么样子,还得拭目以待。 diff --git "a/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\272\214\347\212\266\346\200\201\345\222\214\344\270\212\344\270\213\346\226\207.md" "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\272\214\347\212\266\346\200\201\345\222\214\344\270\212\344\270\213\346\226\207.md" new file mode 100644 index 0000000..6e048b6 --- /dev/null +++ "b/khipu/khipu\345\271\266\350\241\214\345\210\206\346\236\220\344\271\213\344\272\214\347\212\266\346\200\201\345\222\214\344\270\212\344\270\213\346\226\207.md" @@ -0,0 +1,480 @@ +# khipu并行分析之二状态和上下文 + +## 1、并行的状态控制 +并行的状态控制主要有两个一个是WorldState.scala,另外一个是BlockWorldState.scala。从形式上来看二者是组合的关系,但实际应该是依赖或者实现的关系,不太明白Scala的用法。 +
+ +``` +object BlockWorldState { + + sealed trait RaceCondition + case object OnAddress extends RaceCondition + case object OnAccount extends RaceCondition + case object OnStorage extends RaceCondition + case object OnCode extends RaceCondition + ...... + def apply( + blockchain: Blockchain, + accountNodeStorage: NodeKeyValueStorage, + storageNodeStorage: NodeKeyValueStorage, + accountStartNonce: UInt256, + stateRootHash: Option[Hash] = None +): BlockWorldState = { + + /** + * Returns an accounts state trie "The world state (state), is a mapping + * between Keccak 256-bit hashes of the addresses (160-bit identifiers) and account states + * (a data structure serialised as RLP [...]). + * Though not stored on the blockchain, it is assumed that the implementation will maintain this mapping in a + * modified Merkle Patricia tree [...])." + * + * See [[http://paper.gavwood.com YP 4.1]] + */ + val underlyingAccountsTrie = MerklePatriciaTrie[Address, Account]( + stateRootHash.getOrElse(Hash(trie.EmptyTrieHash)).bytes, + accountNodeStorage + )(Address.hashedAddressEncoder, Account.accountSerializer) + + new BlockWorldState( + blockchain, + accountNodeStorage, + storageNodeStorage, + accountStartNonce, + blockchain.evmCodeStorage, + TrieAccounts(underlyingAccountsTrie), + Map(), + Map(), + Map(), + Map(), + Set(), + None + ) +} +} +``` +
+在他的下面是伴生类BlockWorldState +
+ + ``` + final class BlockWorldState private ( + blockchain: Blockchain, + accountNodeStorage: NodeKeyValueStorage, + storageNodeStorage: NodeKeyValueStorage, + accountStartNonce: UInt256, + evmCodeStorage: EvmCodeStorage, + private var trieAccounts: TrieAccounts, + private var trieStorages: Map[Address, TrieStorage], + private var codes: Map[Address, ByteString], + private var accountDeltas: Map[Address, Vector[BlockWorldState.AccountDelta]], + private var raceConditions: Map[BlockWorldState.RaceCondition, Set[Address]], + private var touchedAddresses: Set[Address], // for debug + private var stx: Option[SignedTransaction] // for debug + ) extends WorldState[BlockWorldState, TrieStorage] + ``` +
+通过上面的类和它的伴生对象可以看出,竞态条件并不太多,只有四个,即OnAddress、OnAccount、OnStorage、OnCode。和前面的说明是呼应的,其实在合并状态时,重点还是关注前三个,Code在执行时就会进行处理。 +
+在状态的类成员中,有大量的状态获取函数,用来从当前世界状态中取得当前的状态值。事务的回滚也是靠这些数据来实现的,同时,由于只考虑记载当前交易状态,所以取消了对锁的控制。 + +## 2、并行的上下文 +并行的上下文主要有两个类: +
+ +``` +object ProgramState { + trait ParallelRace + case object OnAccount extends ParallelRace + case object OnError extends ParallelRace +} +/** + * Intermediate state updated with execution of each opcode in the program + * + * @param context the context which initiates the program + * @param gas current gas for the execution + * @param stack current stack + * @param memory current memory + * @param pc program counter - an index of the opcode in the program to be executed + * @param returnData data to be returned from the program execution + * @param gasRefund the amount of gas to be refunded after execution (not sure if a separate field is required) + * @param addressesToDelete list of addresses of accounts scheduled to be deleted + * @param halted a flag to indicate program termination + * @param error indicates whether the program terminated abnormally + */ +final class ProgramState[W <: WorldState[W, S], S <: Storage[S]](val context: ProgramContext[W, S], val isDebugTraceEnabled: Boolean) { + import ProgramState._ + + var gas: Long = context.startGas + var world: W = context.world + var addressesToDelete: Set[Address] = context.initialAddressesToDelete + var addressesTouched: Set[Address] = context.initialAddressesTouched + + var pc: Int = 0 + var returnData: ByteString = ByteString() + var gasRefund: Long = 0 + var txLogs: Vector[TxLogEntry] = Vector() + private var _halted: Boolean = false + var error: Option[ProgramError] = None + private var _isRevert: Boolean = false + + var returnDataBuffer: ByteString = ByteString() + + private var _parallelRaceConditions = Set[ParallelRace]() + + val stack: Stack = Stack.empty() + val memory: Memory = Memory.empty() + +...... + + def parallelRaceConditions = _parallelRaceConditions + def withParallelRaceCondition(race: ParallelRace) = { + this._parallelRaceConditions += + this + } + def mergeParallelRaceConditions(races: Set[ParallelRace]) = { + this._parallelRaceConditions ++= racesrace + this + } + +...... +} +``` +
+另外一个是程序的上下文: +
+ +``` +object ProgramContext { + def apply[W <: WorldState[W, S], S <: Storage[S]]( + stx: SignedTransaction, + recipientAddress: Address, + program: Program, + blockHeader: BlockHeader, + world: W, + config: EvmConfig, + initialAddressesToDelete: Set[Address], + initialAddressesTouched: Set[Address], + isStaticCall: Boolean + ): ProgramContext[W, S] = { + + // YP eq (91) + val inputData = if (stx.tx.isContractCreation) ByteString() else stx.tx.payload + + val env = ExecEnv( + recipientAddress, + stx.sender, + stx.sender, + stx.tx.gasPrice, + inputData, + stx.tx.value, + program, + blockHeader, + callDepth = 0 + ) + + val startGas = stx.tx.gasLimit - config.calcTransactionIntrinsicGas(stx.tx.payload, stx.tx.isContractCreation) + + ProgramContext(env, recipientAddress, startGas, world, config, initialAddressesToDelete, initialAddressesTouched, isStaticCall) + } +} + +/** + * Input parameters to a program executed on the EVM. Apart from the code itself + * it should have all (interfaces to) the data accessible from the EVM. + * + * @param env set of constants for the execution + * @param targetAddress used for determining whether a precompiled contract is being called (potentially + * different from the addresses defined in env) + * @param startGas initial gas for the execution + * @param world provides interactions with world state + * @param config evm config + * @param initialAddressesToDelete contains initial set of addresses to delete (from lower depth calls) + */ +final case class ProgramContext[W <: WorldState[W, S], S <: Storage[S]]( + env: ExecEnv, + targetAddress: Address, + startGas: Long, + world: W, + config: EvmConfig, + initialAddressesToDelete: Set[Address], + initialAddressesTouched: Set[Address], + isStaticCall: Boolean +) +``` +
+这两个类互相扶持,掌握着程序的上下文的状态,可以从其中得到世界状态,或者这样说,通过programstate来进行程序和并行交易的控制,包括并行的数量设置,都在这个类中。 +
+ +## 3、并行状态和上下文的更新和合并 +状态和上下文结合后,开始在两个地方进行处理,一个是VM,一个是Ledger中。基本上就是在执行区块这个函数命令中,来回穿梭调用两个相关的依赖对象。 +
+ +``` +case object BALANCE extends OpCode[UInt256](0x31, 1, 1) with ConstGas[UInt256] { + protected def constGasFn(s: FeeSchedule) = s.G_balance + protected def getParams[W <: WorldState[W, S], S <: Storage[S]](state: ProgramState[W, S]) = { + val List(accountAddress) = state.stack.pop() + accountAddress + } + + protected def exec[W <: WorldState[W, S], S <: Storage[S]](state: ProgramState[W, S], params: UInt256): ProgramState[W, S] = { + val accountAddress = params + val accountBalance = state.world.getBalance(Address(accountAddress)) + state.stack.push(accountBalance) + state.withParallelRaceCondition(ProgramState.OnAccount).step() + } +} +``` +
+
+合并在块状态中的代码: +
+ +``` +private[ledger] def commit(): BlockWorldState = { + trieAccounts = trieAccounts.commit() + this + } + + /** + * Should be called adter committed + */ + def persist(): BlockWorldState = { + // deduplicate codes first + this.codes.foldLeft(Map[Hash, ByteString]()) { + case (acc, (address, code)) => acc + (Hash(crypto.kec256(code)) -> code) + } foreach { + case (hash, code) => evmCodeStorage + (hash -> code) + } + + this.trieStorages.foreach { + case (address, storageTrie) => storageTrie.underlying.persist() + } + + this.trieAccounts.underlying.persist() + + this + } + + // --- merge --- + + def mergeRaceConditions(later: BlockWorldState): BlockWorldState = { + later.raceConditions foreach { + case (k, vs) => this.raceConditions += (k -> (this.raceConditions.getOrElse(k, Set()) ++ vs)) + } + this + } + + private[ledger] def merge(later: BlockWorldState): Either[Map[RaceCondition, Set[Address]], BlockWorldState] = { + val raceCondiftions = this.raceConditions.foldLeft(Map[RaceCondition, Set[Address]]()) { + case (acc, (OnAccount, addresses)) => acc + (OnAccount -> addresses.filter(later.trieAccounts.logs.contains)) + case (acc, (OnStorage, addresses)) => acc + (OnStorage -> addresses.filter(later.trieStorages.contains)) + case (acc, (OnCode, addresses)) => acc + (OnCode -> addresses.filter(later.codes.contains)) + case (acc, (OnAddress, addresses)) => acc + (OnAddress -> addresses.filter(x => later.codes.contains(x) || later.trieStorages.contains(x) || later.trieAccounts.logs.contains(x))) + } filter (_._2.nonEmpty) + + if (raceCondiftions.isEmpty) { + val toMerge = this.copy + toMerge.touchedAddresses ++= later.touchedAddresses + //mergeAccountTrieAccount_simple(toMerge, that) + toMerge.mergeAccountTrieAccount(later).mergeTrieStorage(later).mergeCode(later).mergeRaceConditions(later) + Right(toMerge) + } else { + Left(raceCondiftions) + } + } + + /** mergeAccountTrieAccount should work now, mergeAccountTrieAccount_simple is left here for reference only */ + private def mergeAccountTrieAccount_simple(later: BlockWorldState): BlockWorldState = { + this.trieAccounts.logs ++= later.trieAccounts.logs + this + } + + private def mergeAccountTrieAccount(later: BlockWorldState): BlockWorldState = { + val alreadyMergedAddresses = later.accountDeltas map { + case (address, deltas) => + val valueMerged = deltas.foldLeft(this.getAccount(address).getOrElse(this.emptyAccount)) { + case (acc, AccountDelta(nonce, balance, _, _)) => acc.increaseNonce(nonce).increaseBalance(balance) + } + + // just put the lasted stateRoot and codeHash of y and merge delete + later.trieAccounts.logs.get(address).map { + case Updated(Account(_, _, stateRoot, codeHash)) => this.trieAccounts += (address -> valueMerged.withStateRoot(stateRoot).withCodeHash(codeHash)) + case Original(_) => this.trieAccounts += (address -> valueMerged) + case Deleted(_) => this.trieAccounts -= address + } + + address + } toSet + + this.trieAccounts.logs ++= (later.trieAccounts.logs -- alreadyMergedAddresses) + this + } + + private def mergeTrieStorage(later: BlockWorldState): BlockWorldState = { + this.trieStorages ++= later.trieStorages + this + } + + private def mergeCode(later: BlockWorldState): BlockWorldState = { + this.codes ++= later.codes + this + } +``` +
+merge函数里首先根据竞态条件依次进行合并处理,然后再合并相关状态树等。这也和最初提出的三个方向基本保持一致。这个合并函数在并行执行结果处被调用 +
+ +``` +prevWorld.merge(txResult.world) match { + case Left(raceCondiftions) => + log.debug(s"tx ${txResult.stx.hash} has race conditions with prev world state:\n$raceCondiftions") + reExecute(txResult.stx, prevWorld) + + case Right(mergedWorld) => + parallelCount += 1 + currWorld = Some(mergedWorld) + txResults = txResults :+ txResult +} +``` +
+涉及到一个问题,就是Reward的计算,处理它使用了类似的机制,分三步进行: +
+1、预计算 +
+ +``` +override def prepareBlock( + block: Block, + validators: Validators +)(implicit executor: ExecutionContext): Future[BlockPreparationResult] = { + val parentStateRoot = blockchain.getBlockHeaderByHash(block.header.parentHash).map(_.stateRoot) + val initialWorld = blockchain.getReadOnlyWorldState(None, blockchainConfig.accountStartNonce, parentStateRoot) + + executePreparedTransactions(block.body.transactionList, initialWorld, block.header, validators.signedTransactionValidator) map { + case (execResult @ BlockResult(resultingWorldState, _, _, _), txExecuted) => + val worldRewardPaid = payBlockReward(block)(resultingWorldState) + val worldPersisted = worldRewardPaid.commit().persist() + BlockPreparationResult(block.copy(body = block.body.copy(transactionList = txExecuted)), execResult, worldPersisted.stateRootHash) + } +} +``` +
+2、执行中计算 +
+ +``` +override def executeBlock(block: Block, validators: Validators)(implicit executor: ExecutionContext): Future[Either[BlockExecutionError, BlockResult]] = { + val start1 = System.nanoTime + val parallelResult = executeBlockTransactions(block, validators.signedTransactionValidator, isParallel = true && !blockchainConfig.isDebugTraceEnabled) map { + case Right(blockResult) => + log.debug(s"${block.header.number} parallel-executed in ${(System.nanoTime - start1) / 1000000}ms") + + val start2 = System.nanoTime + val worldRewardPaid = payBlockReward(block)(blockResult.world) + } + ...... +} +``` +
+3、并行结果后计算 +
+ +``` +parallelResult flatMap { + case Right((blockResult, worldCommitted)) => Future.successful(Right(blockResult)) + + case left @ Left(error) => + log.debug(s"in parallel failed with error $error, try sequential ...") + + val start1 = System.nanoTime + executeBlockTransactions(block, validators.signedTransactionValidator, isParallel = false) map { + case Right(blockResult) => + log.debug(s"${block.header.number} sequential-executed in ${(System.nanoTime - start1) / 1000000}ms") + + val worldRewardPaid = payBlockReward(block)(blockResult.world) + } + } +``` +
+4、计算函数 +
+ +``` +private def payBlockReward(block: Block)(world: BlockWorldState): BlockWorldState = { + val minerAddress = Address(block.header.beneficiary) + val minerAccount = getAccountToPay(minerAddress)(world) + val minerReward = blockRewardCalculator.calcBlockMinerReward(block.header.number, block.body.uncleNodesList.size) + val afterMinerReward = world.saveAccount(minerAddress, minerAccount.increaseBalance(minerReward)) + log.debug(s"Paying block ${block.header.number} reward of $minerReward to miner with account address $minerAddress") + + block.body.uncleNodesList.foldLeft(afterMinerReward) { (ws, ommer) => + val ommerAddress = Address(ommer.beneficiary) + val account = getAccountToPay(ommerAddress)(ws) + val ommerReward = blockRewardCalculator.calcOmmerMinerReward(block.header.number, ommer.number) + log.debug(s"Paying block ${block.header.number} reward of $ommerReward to ommer with account address $ommerAddress") + ws.saveAccount(ommerAddress, account.increaseBalance(ommerReward)) + } +} +``` +
+5、最终给付 +
+ +``` +private def postExecuteTransactions( + blockHeader: BlockHeader, + evmCfg: EvmConfig, + txResults: Vector[TxResult], + stats: Stats +)(world: BlockWorldState): Either[BlockExecutionError, BlockResult] = { + try { + val (accGas, accTxFee, accTouchedAddresses, accReceipts) = txResults.foldLeft(0L, UInt256.Zero, Set[Address](), Vector[Receipt]()) { + case ((accGas, accTxFee, accTouchedAddresses, accReceipts), TxResult(stx, worldAfterTx, gasUsed, txFee, logs, touchedAddresses, _, error, isRevert, _)) => + + val postTxState = if (evmCfg.eip658) { + if (error.isDefined || isRevert) Receipt.Failure else Receipt.Success + } else { + worldAfterTx.stateRootHash + //worldAfterTx.commit().stateRootHash // TODO here if get stateRootHash, should commit first, but then how about parallel running? how about sending a lazy evaulate function instead of value? + } + + log.debug(s"Tx ${stx.hash} gasLimit: ${stx.tx.gasLimit}, gasUsed: $gasUsed, cumGasUsed: ${accGas + gasUsed}") + + val receipt = Receipt( + postTxState = postTxState, + cumulativeGasUsed = accGas + gasUsed, + logsBloomFilter = BloomFilter.create(logs), + logs = logs + ) + + (accGas + gasUsed, accTxFee + txFee, accTouchedAddresses ++ touchedAddresses, accReceipts :+ receipt) + } + + //计算并给付GAS + val minerAddress = Address(blockHeader.beneficiary) + val worldPayMinerForGas = world.pay(minerAddress, accTxFee) + + // find empty touched accounts to be deleted + val deadAccounts = if (evmCfg.eip161) { + (accTouchedAddresses + minerAddress) filter (worldPayMinerForGas.isAccountDead) + } else { + Set[Address]() + } + //log.debug(s"touched accounts: ${result.addressesTouched}, miner: $minerAddress") + log.debug(s"dead accounts accounts: $deadAccounts") + val worldDeletedDeadAccounts = deleteAccounts(deadAccounts)(worldPayMinerForGas) + + log.debug(s"$blockHeader, accGas $accGas, receipts = $accReceipts") + Right(BlockResult(worldDeletedDeadAccounts, accGas, accReceipts, stats)) + } catch { + case MPTNodeMissingException(_, hash, table) => Left(MissingNodeExecptionError(blockHeader.number, hash, table)) + case e: Throwable => throw e + } +} +``` +
+最终更新到世界状态中做为下一次区块并行交易的起点状态。 + +## 4、总结 +通过上面的分析发现,khipu的并行思路不错,简单有效,而且基于目前网络上真正能够冲突的数据还是比较少的实际环境,提高的速度还是比较明显的。在下一篇,详细分析并行的过程。 diff --git a/libra/img/bec-of.png b/libra/img/bec-of.png new file mode 100644 index 0000000..32f4d9c Binary files /dev/null and b/libra/img/bec-of.png differ diff --git a/libra/img/compiler.png b/libra/img/compiler.png new file mode 100644 index 0000000..a8d470e Binary files /dev/null and b/libra/img/compiler.png differ diff --git a/libra/img/expressions.png b/libra/img/expressions.png new file mode 100644 index 0000000..c1930b1 Binary files /dev/null and b/libra/img/expressions.png differ diff --git a/libra/img/vm.png b/libra/img/vm.png new file mode 100644 index 0000000..5094e94 Binary files /dev/null and b/libra/img/vm.png differ diff --git "a/libra/libra\347\232\204\346\224\257\344\273\230\345\210\206\346\236\220.md" "b/libra/libra\347\232\204\346\224\257\344\273\230\345\210\206\346\236\220.md" new file mode 100644 index 0000000..c32b3e5 --- /dev/null +++ "b/libra/libra\347\232\204\346\224\257\344\273\230\345\210\206\346\236\220.md" @@ -0,0 +1,126 @@ +# libra的支付分析 + +## 一、背景 +#### 1、libra的介绍 +libra是fackbook公司新推出的一种加密货币(支付系统),它架构在了区块链(或者说类似区块链)系统上。它的主要目的是实现全球支付结算,实现全球无障碍支付,特别是对广大的没有银行帐户的人们,它提供了一个巨大的契机。 +
+libra是以抵押一篮子货币(或者相关资产)来获得资源(在libra中不叫价值叫资源)。因此网上很多的专业人士认为其并不是严格意义上的数字货币,而只是一个支付系统或者说一种支付的手段。 +
+libra的帐户管理是中心化的,但帐户生成的相关密钥是分布式生成的。通过基金会的形式进行治理,采用100个认证节点来做为伪中心节点。因此,libra和EOS在这方面本质上没有区别。 + +#### 2、同其它(微信和支付宝等)的不同 +微信和支付宝做为一种支付手段和结算系统,是直接使用国家货币,但是无法进行跨国支付(严格的说是很难进行跨国支付)。无法对银行和大型金融机构进行清算。即使在某一些国家引入了其它的金融机构,汇率和跨国银行间的交易仍然使用的是原有的金融结算系统。资金成本和时间成本的问题仍然没有解决。 +
+从技术角度来看,二者的帐户管理都采用了中心化的管理机制,但底层的技术支持,微信和支付宝仍然采用的传统的银行的管理手段,即中心化技术支持方式。换句话说,如果支付宝公司有一天关机,那么整个交易系统会停止。但是libra采用的区块链系统,可以认为只要100个节点中有相当一部分运行,那么,整个系统就会正常运行。 +
+libra可以通过MOVE语言进行资产的定义(可以理解成类似于ERC2.0),但微信和支付宝则不可以重新定义资产。libra可以锚定很多货币,但是目前看微信和支付宝只能锚定本国货币。 +
+同样,我们也注意到了不久前JP摩根的coin,相对于它来说,libra更类似于真正意义上的法币。而摩根币则只是内部结算的一种手段。 +
+当然,做为一种加密货币,它们都具有可以进行支付,方便进行存入和提取的功能。相对于微信等,libra使用了分布式的钱包,所以其的应用的体验在短时间内可能不如前者更好。 +
+由此,基于现行的资料可以推断libra更倾向于实现一个权利受到限制或者监管的类似于全球央行的一个系统。这个必然会引起各国货币管理者的警惕。从经济学的角度来看,如果libra实现了这个愿景,那么各国通过货币手段来调节经济周期的方式将减弱甚至失效。因此,libra实现其愿景的难度是相当之大。 +## 二、支付的基础和问题 +#### 1、创建了MOVE语言 +通过MOVE语言可以实现资产的安全管理。 +#### 2、金融机构的合作 +银行等金融机构为什么愿意同FACEBOOK合作,主要是其全球高达二十多亿的海量用户,其衍生出来的相关金融收入将是一个天文数字。 +#### 3、KYC身份验证 +之所以把libra看作一个拥抱监管的区块链的一个主要原因就在于此,KYC身份验证即通过政府签发的身份证明进行注册帐户,这样就无可避免的和当前区块链的思想碰撞。无法天然对隐私进行保护。 +#### 4、使用BFT共识 +这是一种减低共识消耗的一种新的BFT算法,其论文的第一作者为中国的尹茂帆同学。 +#### 5、基金会和治理 +区块链的治理和激励是主要的一个问题,一般来说,传统的区块链都是链上治理,但在libra上,采用的链下治理,即通过基金会管理,对理事会的创始成员(即认证节点)的投票(一节点一票)结果负责。同时,它还有不少于5名但不多于19名的协会董事会,用来监督理事会。并且还由非营利机构和学术机构组成5~7名的社会影响力咨询委员会。 +#### 6、和美元的关系 +从长远角度考虑,如果不搀杂政治等其它外因,对美元必定是弱化的,但是从中短期和政治原因考虑,反而是促进美元的霸权地位的。 +#### 7、面临外在的问题 +支付的安全性:黑客对这种便捷的支付手段,一定会非常感兴趣。 +
+全球的金融监管的不确定性:对主权货币的冲击必然会引起各国政府的严厉管控。 +
+资金抵押的安全性:抵押的各种资产,会不会因为长期抵押因各种原因产生灭失、转移和贬值等。 +
+金融风险:最典型的就是经济危机和各种金融犯罪,诸如洗钱等。 +#### 8、面临的内在问题 +其内在的主要问题包含以下几个方面:治理的具体动作流程;国际支付的便捷性和费用;是否对开发者开放:是否满足国际支付的交易量等等。 + +## 三、目前的交易 +当前开往的实验网络交易基本步骤如下: +
+1、注册帐户。 +
+2、转帐,帐号会和一个序列号进行绑定,每转出一次资金,这个序号加一,目的是为了解决分布式的状态的顺序问题,这个在分布式的系统里基本都是有这种方法的。查看自己的帐户信息时,可以用 命令+序号(或地址)来查看。 +
+3、交易过程不产生交易ID而是生成一个二元组或者说交易ID是一个二元组,包含<转账人地址,序列号>, +
+3、确认交易,完成。 +
+4、根据目前的实现状况,初步估计其交易TPS在1000左右。 +## 四、和目前区块链的不同 +1、libra的区块是一个逻辑概念,并不是真正存在。,它只是一组签名状态,所以实际上并不存在传统区块链的链的概念。 +
+2、其数据有点类似于以太坊的三个树,有记录交易,有记录状态,有记录事件。其实就是不多的几个巨大的区块或者说数据库,不断的向其中写入状态和验证的数据。 +
+3、因为没有实际的区块,也就没有挖矿收入,挖矿只是虚拟意义上的挖矿。所以如何进行生态的的建设与现有区块链有所不同。 +
+4、防篡改使用BFT技术,类似于现在的分布式的数据库的技术。但并不是传统的BFT需要耗费大量的时间和网络带宽。 +
+5、在路线图中,是有从联盟到公链的演进,这个有待观察。技术上有不少的难度 。 +
+6、libra是需要抵押资金的,所以其的合约中称之为资源(线性逻辑的类型)而不是Value。所以其加密货币不是传统区块链挖矿而来的。 +
+ +## 五、安全实现 +#### 1、MOVE语言的安全性 +a)First-class assets(一等公民检测): +
+一等公民这个概念是函数式编程语言中的概念,其基本语义如下: +
+First Class:允许成为函数的参数和返回值,允许赋值给变量。 +
+Second Class:只允许作为函数的参数。 +
+Third Class:连成为函数参数都不允许。 +
+b)字节的可验证性(Verifiability): +
+这个当然无法解决全部的字节验证性,那个的性能损失太大了,但是在关键的部位或者关键的阶段进行字节检查是一个最优的选择。主要步骤包括: +控制流图构建;栈高度检查;类型检查;资源检查;引用检查;全局状态链接。 +举一个简单的例子,如果想把一个币移动到另外一个帐户,程序员不小心把move写成了copy,而资源coin是不允许拷贝的,那么字节码检测时就会发现这个问题,并进行保护。同时,资源型仅允许move一次。 +
+c)扩展了Module +
+在MOVE语言中对module这个传统的概念进行了扩展,包含: +
+Transaction Script:一组顺序执行的不保存的一次性的的语句。 +
+Module:资产相关的操作方法的集合。 +
+d)形式化验证: +
+正在开发中,这个得看最后的结果。 +
+e)放弃了一些对安全有影响的功能 +
+比如Dynamic Dispatch,循环递归依赖,Limited mutability等。 +
+f)回归传统的静态语言。 + +#### 2、 HotStuff 算法 +经典 BFT 共识由于通信的复杂度,一般容易有限制。而HotStuff的算法框架是树/链结构,这个有点类似于区块链。它通过最近一次成功获得大部分投票的块决定,但HotStuff 仍然处于BFT的范畴。HotStuff 的核心换届算法统一了传统的正常和特殊两种流程。即没有显式的换届特殊处理,也可以认为是潜在地处处换届。这导致的结果是一个HotStuff的算法实现只需要几十到上百行代码。同时,算法本身对工程师是友好的,实现了从提高性能和保证正确性在算法层面的解耦。HotStuff的选举过程消耗的时间降低到了线性时间。也就是说,它将BFT的共识算法提高到了可以在商业系统上广泛应用的可能。 + +## 六、影响 +1、首先是对稳定币USDT等的冲击。 +
+2、对弱小国家的主权货币的冲击。 +
+3、有可能与美元结合形成数字货币领域内的美元霸权。 +
+4、对POS系统的冲击。 +
+5、对国际清算系统和跨境支付的深远影响。 +
+6、与区块链数字货币(比特币等)的去监管相悖的冲击。 + +## 七、总结 +libra的出现,至少在目前的情况下,对区块链行业起到了一个提振信心的巨大作用。其与传统的区块链既有融合又有独自的特点。在设计上,对政府组织进行了适当的妥协,在技术上,又创建了新的BFT和MOVE语言。从目前来看,应该不失于一个加密货币推向真正应用的良好的折衷解决方案。 diff --git "a/libra/move\350\257\255\350\250\200\347\232\204\351\207\215\347\202\271\345\210\206\346\236\220.md" "b/libra/move\350\257\255\350\250\200\347\232\204\351\207\215\347\202\271\345\210\206\346\236\220.md" new file mode 100644 index 0000000..08b75d2 --- /dev/null +++ "b/libra/move\350\257\255\350\250\200\347\232\204\351\207\215\347\202\271\345\210\206\346\236\220.md" @@ -0,0 +1,804 @@ +# MOVE语言的重点分析 + +## 一、MOVE语言的介绍 +MOVE语言借鉴了相当一部分的RUST和c++中的编程的优秀部分,并结合智能合约编写的实际情况,创建了不同于以太坊和EOS的一种静态智能合约平台开发语言。它是facebook为其libra准备的智能合约的开发语言,它从语法层面和二进流的字节两个方向上都对合约的安全进行了有效的控制。不过,目前其正在发展过程中,许多提出的目标能否最终很好的实现,仍待观察。 + +## 二、安全层面的分析 +1、与其它相关语言的对比 +
+以太坊的solidity:新语言,语言的功能简单,语法简单,无法实现强大的功能。出现了很多语言级别的漏洞,比如溢出(最近的美图的漏洞)、超越栈深度(1024)、DAO等。 +看一下例子: +``` +//溢出 +pragma solidity ^0.4.5; +contract Test { + //上溢出---直接归0 + function TestOF() returns (uint256 of) { + uint256 v = 2**256 - 1; + return v + 1; + } ​ + //下溢出 + function TestUF() returns (uint256 uf) { + uint256 v = 0; + return v - 1; + } + } +``` +如果在Remix的编译器中,会直接提出编译警告,有溢出的风险。如果在转帐过程中有类似的操作,可能就风险大多了,看一下美图的漏洞: +
+ +![bec](img/bec-of.png) +
+这个就是以太坊ERC-20智能合约BatchOverFlow数据溢出的漏洞,这和上面的溢出没有本质区别。当然,Solidity后来也提供了安全的数学库,算是亡羊补牢吧。 +
+EOS:使用c++,入门较难,功能强大,虽然有相当多的一部分c++功能无法使用。所以用它写出来的智能合约,基本上语言层面的漏洞不多或者说基本不占主流(毕竟c++语言应用多年,好多坑都已经被踩了)。大多是逻辑或者链设计上的问题(比如回滚攻击,延迟攻击),但是这并不代表用c++写智能合约是多么完美。 +看一下下面GITHUB上的一个例子: +
+ +``` +//https://github.com/itleaks/eos-contract/blob/master/rollback-exp/rollback/rollback.cpp +class Rollback : public eosio::contract { + public: + using contract::contract; + + /// @abi action + void dream(account_name who, asset value, string memo) { + require_auth( _self ); + auto eos_token = eosio::token(N(eosio.token)); + //获得原始余额 + auto balance = eos_token.get_balance(_self, symbol_type(S(4, EOS)).name()); + //开始游戏,即转帐给游戏方 + action( + permission_level{ _self, N(active) }, + N(eosio.token), N(transfer), + std::make_tuple(_self, who, value, memo) + ).send(); + //调用reality函数检测游戏结果,成功就得到奖励,否则回滚 + action( + permission_level{ _self, N(active) }, + _self, N(reality), + std::make_tuple(balance) + ).send(); + } + + /// @abi action + //通过新旧帐户余额的比较,判断是否中奖。 + void reality( asset data) { + require_auth( _self ); + auto eos_token = eosio::token(N(eosio.token)); + auto newBalance = eos_token.get_balance(_self, symbol_type(S(4, EOS)).name()); + eosio_assert( newBalance.amount > data.amount, "bad day"); + // action( + // permission_level{ _self, N(active) }, + // _self, N(test), + // std::make_tuple(newBalance) + // ).send(); + } +}; + +EOSIO_ABI( Rollback, (dream)(reality)) +``` +
+所以说使用成熟的语言的好处是,语言级别的漏洞少而且很容易被开发人员发现和修改。 +MOVE:使用了(线性类型)资源类型。再引入了c++中的move,copy的相似功能。安全控制住了资源的无序利用。从理论上讲,大大减少了语言级别上和部分逻辑上(比如资源的连续增减的逻辑二义性)的漏洞,看一下白皮书的例子: +
+ +``` +module Currency { +resource Coin { value: u64 } +// ... +} +public deposit(payee: address, to_deposit: Coin) { +let to_deposit_value: u64 = Unpack(move(to_deposit)); +let coin_ref: &mut Coin = BorrowGlobal(move(payee)); +let coin_value_ref: &mut u64 = &mut move(coin_ref).value; +let coin_value: u64 = *move(coin_value_ref); +*move(coin_value_ref) = move(coin_value) + move(to_deposit_value); +} +public withdraw_from_sender(amount: u64): Coin { +let transaction_sender_address: address = GetTxnSenderAddress(); +let coin_ref: &mut Coin = BorrowGlobal(move(transaction_sender_address)); +let coin_value_ref: &mut u64 = &mut move(coin_ref).value; +let coin_value: u64 = *move(coin_value_ref); +RejectUnless(copy(coin_value) >= copy(amount)); +*move(coin_value_ref) = move(coin_value) - copy(amount); +let new_coin: Coin = Pack(move(amount)); +return move(new_coin); +} +``` +
+首先通过抽象封装了对Coin的操作,使得对外是不可见的,资源的操作只能通过Currency来进行,而Coin做为资源,只能通过move来操作,而一旦move成功,就自然失去了对Coin的原来的所有权,这样就可以防止对Coin的反复操作,等同于从语法层面对资源重复操作进行了限制。可以通过Unpack和BorrowGlobal等内置的接口来对资源的操作进行限制,提高资源的安全性。通过这样一个接收款的两个函数过程,就可以比较清晰的看出MOVE语言对资源操作安全性要求的严格性。另外,它还提供了字节验证过程(Bytecode verifier): +
+ +```rust +//!ResourceTransitiveChecker:对非资源字段中的资源字段的验证 +pub fn verify(self) -> Vec { + let mut errors = vec![]; + for (idx, struct_def) in self.module_view.structs().enumerate() { + let def_is_resource = struct_def.is_resource(); + if !def_is_resource { + let mut fields = struct_def.fields(); + let any_resource_field = fields.any(|field| field.type_signature().is_resource()); + if any_resource_field { + errors.push(VerificationError { + kind: IndexKind::StructDefinition, + idx, + err: VMStaticViolation::InvalidResourceField, + }); + } + } + } + errors +} +``` +
+所以单纯从设计角度来看MOVE语言的安全性确实要比其它两个主流的合约语言考虑的多得多。但是,毕竟MOVE尚在迭代中,并且没有过实际的应用测试案例,其真正落地的效果,还不敢妄下断言。 +
+ +2、形式化验证 +
+常用的Coq可以进行形式化验证,它从更高级的抽象角度通过形式化验证机制来确保智能合约的安全性。在这点上,MOVE语言与其有异曲同式之妙,不同的是,MOVE语言更倾向于单独创建一个验证工具来验证安全性,而形式化验证直接就可以抽象到语言上进行操作。 +
+ +3、安全控制 +
+一般来说在安全控制方面可以从两个方面下手,一个是使用高级的语言(如DeepSEA等),从语法层次上增加大量的安全属性然后在编译器上进行验证;另外一种是使用低级语言,直接在运行时对安全属性进行检察。MOVE语言采取了一种折衷的态度,它使用了一种字节码(IR),然后使用字节码验证器对内存安全、存储安全和资源安全进行了验证。同时它使用了一些特性:抛弃了Dynamic Dispatch(动态分发);静态语言并保持有限的可变性以及扩展的模块功能。 +相对来说,经过这一系列的操作,完全可以可以使用独立的静态验证工具对合约进行有效的检查,确实也是如此,libra的官方也正在着手开发这样一款强大的工具。 + +## 三、MOVE语言的编译角度分析 +在现代的编译器中,一个基本的发展方向是朝着分段式编译发展,目前主流的基本分为三段:前端编译,中部优化(IR)和后端转机器码。下面从这三个阶段来分析一下MOVE语言的现实情况,并窥测一下未来的发展。 +
+1、前端编译 +
+MOVE直接操作类似于IR的语言,所以目前来看其前端编译可以认为没有,至于以后会不会提供更高级的语言或者开发工具来转换,这要看以后的发展。 +
+2、IR中间语言分析 +
+使用IR的优势在于,如果能形成标准,以后任何语言从理论上都可以编译到IR,然后形成与平台或者硬件无关的开发。这点在LLVM出现后,引起了高度的重视,这个方向的发展也相当的迅速。MOVE语言之所以采用这咱类IR的语言方式,应该有也有这种想法。至少在以后朝这个方向完善时,付出的代价不会太大。 +MOVE语言应该是牺牲了图灵完备性,特别是针对金融进行了优化(例如资源的使用)。在编译时的优化工作应该还有很大改善余地,毕竟MOVE还处在初始阶段。比如白皮书中提到的泛型、容器和事件等等。下面是编译器的文件组织: +![compiler](img/compiler.png) +
+在Parser目录下,mod.rs和ast.rs主要用来抽象语法树AST及其依赖的数据结构,在这个过程中,可以对一些基础的语法进行分析处理。这里利用lalr来生成一个解析器。采用的是Rust的一个开源框架LALRPOP(syntax.lalrpop:http://www.github.com/lalrpop/lalrpop)。 +一般来说,编译器会使用词法分析器(lexer)和语法分析器(Parser)来处理输入的代码。词法分析器的构建一般会从正则到NFA,然后转到最小化DFA再到词法生成器的构建。但是MOVE方言的中低级性,应该对词法分析器的要求不高,相对语法分析器的工作量来说,语法分析器的工作量应该比词法分析器多一些。 +在目前的编译器IR中间语言这一段是集中优化的区间,在LLVM的编译器中,会有一个HIR到MIR的过程,在这个过程中,可以调用LLVM的优化编译器opt进行优化,从而这一个阶段得到一个优化后的IR中间代码。 +比如有两种常见的编译器的优化:一种是DCE,一种是常量优化。 +
+看一下在MOVE中编译过程代码(三个主要文件mod.rs,ast.rs和compiler.rs): +
+ +```rust +//mod.rs分析 +//创建脚本 +pub fn parse_script(script_str: &str) -> Result { + let stripped_string = &strip_comments(script_str); + let parser = syntax::ScriptParser::new(); + match parser.parse(stripped_string) { + Ok(script) => Ok(script), + Err(e) => handle_error(e, script_str), + } +} +//创建模块 +pub fn parse_module(modules_str: &str) -> Result { + let stripped_string = &strip_comments(modules_str); + let parser = syntax::ModuleParser::new(); + match parser.parse(stripped_string) { + Ok(module) => Ok(module), + Err(e) => handle_error(e, modules_str), + } +} +//AST的生成: +//结构体类是否为资源类型的枚举声明 +#[derive(Debug, PartialEq, Clone)] +pub enum Kind { + /// `R` + Resource, + /// `V` + Value, +} +//创建一个新的模块 +impl ModuleDefinition { + /// Creates a new `ModuleDefinition` from its string name, dependencies, structs+resources, + /// and procedures + /// Does not verify the correctness of any internal properties of its elements + pub fn new( + name: String, + imports: Vec, + structs: Vec, + functions: Vec<(FunctionName, Function)>, + ) -> Self { + ModuleDefinition { + name: ModuleName::new(name), + imports, + structs, + functions, + } + } +} +//函数和函数签名 +impl FunctionSignature { + /// Creates a new function signature from the parameters and the return types + pub fn new(formals: Vec<(Var, Type)>, return_type: Vec) -> Self { + FunctionSignature { + formals, + return_type, + } + } +} + +impl Function { + /// Creates a new function declaration from the components of the function + /// See the declaration of the struct `Function` for more details + pub fn new( + visibility: FunctionVisibility, + formals: Vec<(Var, Type)>, + return_type: Vec, + annotations: Vec, + body: FunctionBody, + ) -> Self { + let signature = FunctionSignature::new(formals, return_type); + Function { + visibility, + signature, + annotations, + body, + } + } +} +//更多的细节可参看相关代码 +``` +
+语法分析器也即解析器,通过一系列的规则转换成AST(注意和CST的不同,CST是百分百和源码匹配的)。看上面代码比较复杂,看一个简单的例子: +
+ +``` +function Demo(t) { + return t; +} + +//AST:是不是和WASM的文件有些相同 +{ + type: "FunctionDeclaration", + id: { + type: "Identifier", + name: "Demo" + }, + params: [ + { + type: "Identifier", + name: "t" + } + ], + ... +} +``` +
+再来看一下这里语法的规则,先看一下代码中自带的一部分语法的说明: +
+ +![express](img/expressions.png) +
+再看一下提供的lalrpop的语法文件: +
+ +``` +U64: u64 = => u64::from_str(s).unwrap(); +Name: String = => s.to_string(); +ByteArray: ByteArray = { + => { + ByteArray::new(hex::decode(&s[2..s.len()-1]).unwrap_or_else(|_| panic!("The string {:?} is not a valid hex-encoded byte array", s))) + } +}; +AccountAddress: AccountAddress = { + < s: r"0[xX][0-9a-fA-F]+" > => { + let mut hex_string = String::from(&s[2..]); + if hex_string.len() % 2 != 0 { + hex_string.insert(0, '0'); + } + + let mut result = hex::decode(hex_string.as_str()).unwrap(); + let len = result.len(); + if len < 32 { + result.reverse(); + for i in len..32 { + result.push(0); + } + result.reverse(); + } + + assert!(result.len() >= 32); + AccountAddress::try_from(&result[..]) + .unwrap_or_else(|_| panic!("The address {:?} is of invalid length. Addresses are at most 32-bytes long", result)) + } +}; +``` +
+这部分代码是此文件开始两个定义规则:一个是针对ByteArray,另外一个是针对帐户地址的,下面的编译器会根据这个文件并配合上面生成的AST进行编译工作,通过对相匹配的正则的处理,来确定是否合乎编译规则。 +
+在AST中,通过对相关安全属性和语法定义,来控制AST的生成。比如上面看到的资源的枚举体,它就可以用来判断安全属性。 +
+通过上述的分析可以看到,在IR阶段,相对于LLVM等其它中间IR,其实MOVE语言也没做什么实质性(优化等)的动作,相反,倒是简略了相当多的东西。 +
+3、后端处理 +
+其实从严格意义上来讲,在MOVE中目前来看应该都算到后端处理上去,前端和IR中间层都可以简化。这种方式是好是坏,还不敢下结论。 +
+CFG几乎是所有的编译工作中都会遇到,通过节点间的跳变来达到对整个语法树进行分析并生成一个CFG的分析树。 +
+看一下编译的过程: +
+ +```rust +//编译过程 +//CFG信息 +struct ControlFlowInfo { + // A `break` is reachable iff it was used before a terminal node + reachable_break: bool, + // A terminal node is an infinite loop or a path that always returns + terminal_node: bool, +} + +impl ControlFlowInfo { + fn join(f1: ControlFlowInfo, f2: ControlFlowInfo) -> ControlFlowInfo { + ControlFlowInfo { + reachable_break: f1.reachable_break || f2.reachable_break, + terminal_node: f1.terminal_node && f2.terminal_node, + } + } + fn successor(prev: ControlFlowInfo, next: ControlFlowInfo) -> ControlFlowInfo { + if prev.terminal_node { + prev + } else { + ControlFlowInfo { + reachable_break: prev.reachable_break || next.reachable_break, + terminal_node: next.terminal_node, + } + } + } +} +//下面的模块和脚本和上面MOD中的相匹配 +impl<'a> Scope for ModuleScope<'a> { + fn make_string(&mut self, s: String) -> Result { + add_item(s, &mut self.module.string_pool).map(StringPoolIndex::new) + } + + fn make_byte_array(&mut self, buf: ByteArray) -> Result { + add_item(buf, &mut self.module.byte_array_pool).map(ByteArrayPoolIndex::new) + } + + fn make_address(&mut self, addr: AccountAddress) -> Result { + add_item(addr, &mut self.module.address_pool).map(AddressPoolIndex::new) + } + ...... + } +impl<'a> Scope for ScriptScope<'a> { + fn make_string(&mut self, s: String) -> Result { + add_item(s, &mut self.script.string_pool).map(StringPoolIndex::new) + } + + fn make_byte_array(&mut self, buf: ByteArray) -> Result { + add_item(buf, &mut self.script.byte_array_pool).map(ByteArrayPoolIndex::new) + } + + fn make_address(&mut self, addr: AccountAddress) -> Result { + add_item(addr, &mut self.script.address_pool).map(AddressPoolIndex::new) + } + ...... + } +``` +
+编译器会使用不同的方式对表达式,函数,模块等分别进行编译,看一下表达式的例子: +
+ +```rust +fn compile_expression( + &mut self, + exp: &Exp, + code: &mut CodeUnit, + function_frame: &mut FunctionFrame, + ) -> Result { + debug!("compile expression {}", exp); + match exp { + Exp::Move(ref x) => self.compile_move_local(&x.value, code, function_frame), + Exp::Copy(ref x) => self.compile_copy_local(&x.value, code, function_frame), + Exp::BorrowLocal(ref is_mutable, ref x) => { + self.compile_borrow_local(&x.value, *is_mutable, code, function_frame) + } + Exp::Value(cv) => match cv.as_ref() { + CopyableVal::Address(address) => { + let addr_idx = self.make_address(&address)?; + code.code.push(Bytecode::LdAddr(addr_idx)); + function_frame.push()?; + Ok(InferredType::Address) + } + CopyableVal::U64(i) => { + code.code.push(Bytecode::LdConst(*i)); + function_frame.push()?; + Ok(InferredType::U64) + } + CopyableVal::ByteArray(buf) => { + let buf_idx = self.make_byte_array(buf)?; + code.code.push(Bytecode::LdByteArray(buf_idx)); + function_frame.push()?; + Ok(InferredType::ByteArray) + } + CopyableVal::Bool(b) => { + if *b { + code.code.push(Bytecode::LdTrue); + } else { + code.code.push(Bytecode::LdFalse); + } + function_frame.push()?; + Ok(InferredType::Bool) + } + CopyableVal::String(_) => bail!("nice try! come back later {:?}", cv), + }, + Exp::Pack(name, fields) => { + let module_idx = ModuleHandleIndex::new(0); + ...... + function_frame.push()?; + Ok(InferredType::Struct(sh)) + } + Exp::UnaryExp(op, e) => { + self.compile_expression(e, code, function_frame)?; + match op { + UnaryOp::Not => { + code.code.push(Bytecode::Not); + Ok(InferredType::Bool) + } + } + } + Exp::BinopExp(e1, op, e2) => { + self.compile_expression(e1, code, function_frame)?; + self.compile_expression(e2, code, function_frame)?; + function_frame.pop()?; + match op { + BinOp::Add => { + code.code.push(Bytecode::Add); + Ok(InferredType::U64) + } + BinOp::Sub => { + code.code.push(Bytecode::Sub); + Ok(InferredType::U64) + } + ...... + BinOp::BitOr => { + code.code.push(Bytecode::BitOr); + Ok(InferredType::U64) + } + BinOp::BitAnd => { + code.code.push(Bytecode::BitAnd); + Ok(InferredType::U64) + } + BinOp::Xor => { + code.code.push(Bytecode::Xor); + Ok(InferredType::U64) + } + ...... + BinOp::Ge => { + code.code.push(Bytecode::Ge); + Ok(InferredType::Bool) + } + } + } + Exp::Dereference(e) => { + let loc_type = self.compile_expression(e, code, function_frame)?; + code.code.push(Bytecode::ReadRef); + match loc_type { + InferredType::MutableReference(sig_ref_token) => Ok(*sig_ref_token), + InferredType::Reference(sig_ref_token) => Ok(*sig_ref_token), + _ => Ok(InferredType::Anything), + } + } + Exp::Borrow { + ref is_mutable, + ref exp, + ref field, + } => { + let this_type = self.compile_expression(exp, code, function_frame)?; + self.compile_load_field_reference( + this_type, + field, + *is_mutable, + code, + function_frame, + ) + } + } + } +``` +
+编译表达式的过程就是通过不断的迭代来完成对整个表达式的编译过程。 +
+从整个编译过程来看,对比LLVM发现,省略了大量的相关文法操作,同样,在代码中,优化的工作也几乎被简化。从这一点来说,改进的余地还是相当大的。不过因为MOVE语言倾向性IR,导致编译器自然的取消了前端,也意味着其和传统的编译器会减少很工作量。 +
+另外,从编译辅助的角度看,MOVE语言和LLVM中提供的工具相比,既简单又匮乏。在LLVM中汇编可以用llvm-as,符号查看可以用llvm-nm,包括对调试中的支持,不一而足。当然,MOVE语言正在快速迭代中,估计后面会提供类似的工具。但是,缺乏编译工具的支持,意味着在开发编译遇到困难时,会使程序员投入大量的工作,并严重依赖于程序员的开发能力。
+在编译完成后,会自动调用前面提到的字节码验证器。这个也在language的目录下,就不再赘述。 +
+ +## 四、MOVE语言的虚拟机分析 +虚拟机是执行IR编译后的代码的环境,虚拟机的好处肯定是多多了。隔离风险,提高效率等等。 +
+ +![vm](img/vm.png) +
+从图中可以看出来,MOVE的版本迭代非常快,和官方的文档已经无法完全匹配了。 +
+1、文件 +
+在file_format.rs中,抽象了执行的二进制代码的格式,这点有点类似于WASM的文件格式抽象。通过一系列类似于Header、Table、 Offset等命令操作来实现对二进制代码的直接操作,具体的生成在serializer.rs文件中。MOVE目前有两种格式,即:CompiledModule和CompiledScript,下面看一段代码: +
+ +```rust +fn serialize_header(&mut self, binary: &mut Vec) -> Result { + serialize_magic(binary); + binary.push(self.major_version); + binary.push(self.minor_version); + binary.push(self.table_count); + + let start_offset; + if let Some(table_count_op) = self.table_count.checked_mul(9) { + if let Some(checked_start_offset) = + check_index_in_binary(binary.len())?.checked_add(u32::from(table_count_op)) + { + start_offset = checked_start_offset; + } else { + bail!("binary too large, could not compute start offset") + } + } else { + bail!("binary too large, could not compute start offset"); + } + + serialize_table( + binary, + TableType::MODULE_HANDLES, + self.module_handles.0 + start_offset, + self.module_handles.1, + ); +...... + Ok(start_offset) +} +``` +
+这段代码是对标准的二进制的头的序列化,如同ELF或者WASM的文件格式类似,开始要填充一些魔数。 +
+2、指令 +
+在VM的指令定义中,和EVM的有些类似: +
+ +```rust +#[rustfmt::skip] +#[allow(non_camel_case_types)] +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum Opcodes { + POP = 0x01, + RET = 0x02, + BR_TRUE = 0x03, + BR_FALSE = 0x04, + BRANCH = 0x05, + LD_CONST = 0x06, + LD_ADDR = 0x07, + LD_STR = 0x08, + LD_TRUE = 0x09, + LD_FALSE = 0x0A, + COPY_LOC = 0x0B, + MOVE_LOC = 0x0C, + ST_LOC = 0x0D, + LD_REF_LOC = 0x0E, + LD_REF_FIELD = 0x0F, + LD_BYTEARRAY = 0x10, + CALL = 0x11, + PACK = 0x12, + UNPACK = 0x13, + READ_REF = 0x14, + WRITE_REF = 0x15, + ADD = 0x16, + SUB = 0x17, + MUL = 0x18, + MOD = 0x19, + DIV = 0x1A, + BIT_OR = 0x1B, + BIT_AND = 0x1C, + XOR = 0x1D, + OR = 0x1E, + AND = 0x1F, + NOT = 0x20, + EQ = 0x21, + NEQ = 0x22, + LT = 0x23, + GT = 0x24, + LE = 0x25, + GE = 0x26, + ASSERT = 0x27, + GET_TXN_GAS_UNIT_PRICE = 0x28, + GET_TXN_MAX_GAS_UNITS = 0x29, + GET_GAS_REMAINING = 0x2A, + GET_TXN_SENDER = 0x2B, + EXISTS = 0x2C, + BORROW_REF = 0x2D, + RELEASE_REF = 0x2E, + MOVE_FROM = 0x2F, + MOVE_TO = 0x30, + CREATE_ACCOUNT = 0x31, + EMIT_EVENT = 0x32, + GET_TXN_SEQUENCE_NUMBER = 0x33, + GET_TXN_PUBLIC_KEY = 0x34, + FREEZE_REF = 0x35, +} +``` +
+数量上和以太坊没有太大区别,增加了一些特有的REF,MOVE之类的指令。在解析的函数里增加对了指令的匹配操作: +
+ +```rust +fn load_code(cursor: &mut Cursor<&[u8]>, code: &mut Vec) -> BinaryLoaderResult<()> { + let bytecode_count = read_u16_internal(cursor)?; + while code.len() < bytecode_count as usize { + let byte = cursor.read_u8().map_err(|_| BinaryError::Malformed)?; + let bytecode = match Opcodes::from_u8(byte)? { + Opcodes::POP => Bytecode::Pop, +...... + Opcodes::BORROW_REF => { + let idx = read_uleb_u16_internal(cursor)?; + Bytecode::BorrowGlobal(StructDefinitionIndex(idx)) + } + Opcodes::RELEASE_REF => Bytecode::ReleaseRef, + Opcodes::MOVE_FROM => { + let idx = read_uleb_u16_internal(cursor)?; + Bytecode::MoveFrom(StructDefinitionIndex(idx)) + } + Opcodes::MOVE_TO => { + let idx = read_uleb_u16_internal(cursor)?; + Bytecode::MoveToSender(StructDefinitionIndex(idx)) + } +...... + }; + code.push(bytecode); + } + Ok(()) +} +``` +
+同样是使用栈的虚拟机,这里没有什么特别的优势。 +
+3、执行 +
+执行的过程其实就对字节码解析同时进行一系列的验证: +
+ +```rust +//缓存验证 +pub fn get_loaded_module_with_fetcher( + &self, + id: &ModuleId, + fetcher: &F, + ) -> VMRuntimeResult> { + ...... + // Verify the module before using it. + let module = match VerifiedModule::new(module) { + Ok(module) => module, + Err((_, errors)) => { + return Err(VMRuntimeError { + loc: Location::new(), + err: VMErrorKind::Verification( + errors + .into_iter() + .map(|error| VerificationStatus::Dependency(id.clone(), error)) + .collect(), + ), + }) + } + }; + + } +//字节码验证 +fn verify_program( + sender_address: &AccountAddress, + program: &Program, + ) -> Result<(VerifiedScript, Vec), VMStatus> { + // Ensure modules and scripts deserialize correctly. + let script = match CompiledScript::deserialize(&program.code()) { + Ok(script) => script, + Err(ref err) => { + warn!("[VM] script deserialization failed {:?}", err); + return Err(err.into()); + } + }; + if !verify_actuals(&script, program.args()) { + return Err(VMStatus::Verification(vec![VMVerificationStatus::Script( + VMVerificationError::TypeMismatch("Actual Type Mismatch".to_string()), + )])); + } + ...... + + // Run the script and module through the bytecode verifier. + //此处进行字节码验证 + static_verify_program(sender_address, script, modules).map_err(|statuses| { + warn!("[VM] bytecode verifier returned errors"); + statuses.iter().collect() + }) + } + +``` +
+这段代码在得到了字节码后,启动static_verify_program进行验证,这个函数中使用VerifiedScript, VerifiedModule(在bytecode_verifier.rs)两个模块工作。 +在verify.rs中还提供了很多的相关的函数,基本上覆盖了目前MOVE语言提出的验证环节。 + +## 五、字节验证(bytecode_verifier)分析 +字节验证的代码在language/bytecode_verifier下面,在verifiter.rs分别调用处理,主要分为以下几类: +
+1、基础验证 +
+包括几个抽象的验证文件:absint.rs,abstract_interpreter.rs,abstract_state.rs +
+看一个对字段验证的代码: +
+ +```rust +fn is_field_in_struct( + &self, + field_definition_index: FieldDefinitionIndex, + struct_handle_index: StructHandleIndex, +) -> bool { + let field_definition = self.module.field_def_at(field_definition_index); + struct_handle_index == field_definition.struct_ +} +``` +
+2、资源验证 +
+包括resources.rs,nonces.rs,partition.rs,前面的代码已经展示,这里就不再重复。 +
+3、语法验证 +
+包括signature.rs,stack_usage-verifier.rs,struct_defs.rs,看下面的代码: +
+ +```rust +//检测是否包含递归 +pub fn verify(self) -> Vec { + let graph_builder = StructDefGraphBuilder::new(self.module); + let graph = graph_builder.build(); + match toposort(&graph, None) { + Ok(_) => { + // Is the result of this useful elsewhere? + vec![] + } + Err(cycle) => { + let sd_idx = graph[cycle.node_id()]; + vec![VerificationError { + kind: IndexKind::StructDefinition, + idx: sd_idx.into_index(), + err: VMStaticViolation::RecursiveStructDef, + }] + } + } +} +``` +
+4、逻辑验证 +
+ 包括check_duplication.rs,control_folw_graph.rs +
+ +```rust +//重复检查 +pub fn verify(self) -> Vec { + let mut errors = vec![]; + + if let Some(idx) = Self::first_duplicate_element(self.module.string_pool()) { + errors.push(VerificationError { + kind: IndexKind::StringPool, + idx, + err: VMStaticViolation::DuplicateElement, + }) + } +...... +} +``` +
+从字节码验证来看,这里应该还有很多的属性可以增加进来,比如有可能的泛型支持里的具体限定等等。可以看到这里从现有还是未来支持,都仍然有巨大的可改善的空间。在源码中,验证这一块儿基本被独立到专门的模块中了,这和开发者未来规划中单独成立验证的工具估计有暗合之处。 + +## 六、总结 +通过上面几个方面的总结,基本上可以看出MOVE语言仍然是一个很初步的语言,无论从编写的友好度还是编译器的支持以及到安全性的管理上,都没有形成一个整体的有机机制。只能说一个大概的MOVE语言的轮廓已经基本勾勒出来了。 diff --git "a/merkle\346\240\221.pdf" "b/merkle\346\240\221.pdf" new file mode 100644 index 0000000..05088fa Binary files /dev/null and "b/merkle\346\240\221.pdf" differ diff --git a/monoxide/img/attack.jpg b/monoxide/img/attack.jpg new file mode 100644 index 0000000..c3a4f6a Binary files /dev/null and b/monoxide/img/attack.jpg differ diff --git a/monoxide/img/design.jpg b/monoxide/img/design.jpg new file mode 100644 index 0000000..0ba7a7d Binary files /dev/null and b/monoxide/img/design.jpg differ diff --git a/monoxide/img/mining.jpg b/monoxide/img/mining.jpg new file mode 100644 index 0000000..672e2fd Binary files /dev/null and b/monoxide/img/mining.jpg differ diff --git "a/monoxide/monoxide\347\232\204\347\256\200\345\215\225\345\210\206\346\236\220\345\222\214\350\256\250\350\256\272.md" "b/monoxide/monoxide\347\232\204\347\256\200\345\215\225\345\210\206\346\236\220\345\222\214\350\256\250\350\256\272.md" new file mode 100644 index 0000000..8189127 --- /dev/null +++ "b/monoxide/monoxide\347\232\204\347\256\200\345\215\225\345\210\206\346\236\220\345\222\214\350\256\250\350\256\272.md" @@ -0,0 +1,47 @@ +# Monoxide的简单分析和讨论 + +## 一、简介 +这两天Monoxide火得不得了,王博士的论文把大家的热情搞了起来。看了王博士的一些介绍,对Monoxide有一个粗浅的认识。在这里对其中的一些问题进行了简单的讨论和分析,特别是在群里和朋友讨论时,对一些细节有了更深的领会。 +
+毕竟没有源码,所以到真正实现,到底会是什么样子,还需要看整个项目的进展情况。 +
+官方论文地址: +
+https://www.usenix.org/system/files/nsdi19-wang-jiaping.pdf + +## 二、几个问题 + +## 1、共识分组 +整个的Monoxide是一个多链系统,共识组是一个无锁(lock-free)的多链架构,独立完成共识,独立校验和执行交易,独立维护组内用户的状态和历史记录。可以把这个多链系统内的每一个链称之为“共识组”。 +
+到这里大家基本应该明白了,这和分片目的基本是一致的。其实在后面的异步的异步共识组可以简单理解成异步跨链通信(更多的细节得以解释论文的人为主)。 + +![design](img/design.jpg) + +## 2、连弩挖矿 +文中提到了为了提高挖矿的效率,抵抗算力攻击,可以使用连弩挖矿的方式来提高算力。连弩挖矿允许矿工同时参与多个编号连续的共识组 (例如从编号b到b+m-1),每次出块的时候哈希函数将覆盖多个将要出块的块头进行计算,同时这些块头将共用一个Nonce。 + +![mine](img/mining.jpg) + +## 3、算力攻击 +算力攻击的主要原因是因为存在了共识分组,就会导致可能有恶意的攻击方将自己的算力倾注到某一个具体的共识组上,导致极有可能达到百分之五十一的算力控制。而比特币等,由于没有分组,算力攻击其实是对抗的整个网络的算力。而在Monoxide中如果有十个分组,那么,整体的算力就会分成原来的十分之一。随着分组的增多,算力的下降就会下降到危险的地步。 + +![attack](img/attack.jpg) +
+为了解决这个问题,就是引入了连弩挖矿,即: +
+前面提到的算力分散问题是这样的一个攻击模型: 在有n个共识组的Monoxide系统中,全网有效算力为H,每个共识组的有效算力为H/n。攻击者在实施攻击的时候,将其所有物理算力T分配到一个特定共识组,在这个共识组中获得有效算力T。那边当其物理算力超过 T > H/n × 51% 的时候,攻击将可以成功,并构造不一致交易(例如双花交易)。 +
+为了抵御这个算力聚焦的攻击模型,我们的思路是强制矿工将算力分散到各个共识组,使其无法集中算力攻击特定共识组。但在一个去中心化的permissionless系统中,我们无法控制矿工如何分配其物理算力。Monoxide引入了连弩挖矿,其效果是将使得全网的有效算力放大为物理算力的n倍,并且在协议的数据结构层面约束了这种放大后的有效算力必须平均分配到各个共识组,从而规避了这种算力聚焦的攻击模型。 + +## 三、问题的分析讨论 +在上面的三个问题中,其实是互相影响的。正是因为共识分组,才引出算力攻击,然后才出现了连弩挖矿。具体的细节目前还不太清楚,但从目前得到的资料来看,Monoxide所谓的解决了不可能三角问题,还是有值得商榷的地方。 +首先,共识组的划分如果过于弱小,且在最恶劣的情况下(所有的交易都跨链)。文章中说会减半,但是这里说得不太明白,会不会异步的网络交易导致大规模的交易失败,这个还得看具体的说明。 +
+其次,算力的分散和聚焦,按文章中解释其实是一个动态的适应过程,这个过程在运行当中,其实是有风险的。它可能在某个时间段仍会导致算力的聚焦攻击。 +
+最后,在群中和朋友讨论时,还有一个问题不太明白,就是如果多个并发挖矿中(有交集),如果其中一个已经挖矿成功,那么连弩挖矿中的默克尔树是定死的,此时如何进行更换?更换的时机如何确定。在最恶劣的情况下,会不会导致网络算力的大规模浪费? + +## 四、总结 +通过上述的分手,从目前的情况来看Monoxide更象是一个工程化的解决办法————动态适应的分片多链架构,在这其中,通过实际情况的调整,来达到对不可能三角的动态伸缩,实现在某种场景下的最优化,从而提高性能。 +有兴趣的同学们可以看一下论文原文,欢迎大家一起讨论分析。Monoxide的有益探索应该值得肯定和学习。 diff --git a/nebulas/img/blockjudge.png b/nebulas/img/blockjudge.png new file mode 100644 index 0000000..1d88a54 Binary files /dev/null and b/nebulas/img/blockjudge.png differ diff --git a/nebulas/img/blockpool.png b/nebulas/img/blockpool.png new file mode 100644 index 0000000..7daca6c Binary files /dev/null and b/nebulas/img/blockpool.png differ diff --git a/nebulas/img/class-parallel.png b/nebulas/img/class-parallel.png new file mode 100644 index 0000000..0bbb605 Binary files /dev/null and b/nebulas/img/class-parallel.png differ diff --git a/nebulas/img/code-m.png b/nebulas/img/code-m.png new file mode 100644 index 0000000..04e0444 Binary files /dev/null and b/nebulas/img/code-m.png differ diff --git a/nebulas/img/dynasty.png b/nebulas/img/dynasty.png new file mode 100644 index 0000000..b151ec9 Binary files /dev/null and b/nebulas/img/dynasty.png differ diff --git a/nebulas/img/nebulas-overview.png b/nebulas/img/nebulas-overview.png new file mode 100644 index 0000000..823871d Binary files /dev/null and b/nebulas/img/nebulas-overview.png differ diff --git a/nebulas/img/paraller.png b/nebulas/img/paraller.png new file mode 100644 index 0000000..37f908f Binary files /dev/null and b/nebulas/img/paraller.png differ diff --git a/nebulas/img/runtime.png b/nebulas/img/runtime.png new file mode 100644 index 0000000..ff47161 Binary files /dev/null and b/nebulas/img/runtime.png differ diff --git a/nebulas/img/runtimeV8.png b/nebulas/img/runtimeV8.png new file mode 100644 index 0000000..d5c5ba0 Binary files /dev/null and b/nebulas/img/runtimeV8.png differ diff --git a/nebulas/img/workflow.png b/nebulas/img/workflow.png new file mode 100644 index 0000000..90172b2 Binary files /dev/null and b/nebulas/img/workflow.png differ diff --git "a/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\345\271\266\350\241\214\344\272\244\346\230\223\345\210\206\346\236\220.md" "b/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\345\271\266\350\241\214\344\272\244\346\230\223\345\210\206\346\236\220.md" new file mode 100644 index 0000000..8696279 --- /dev/null +++ "b/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\345\271\266\350\241\214\344\272\244\346\230\223\345\210\206\346\236\220.md" @@ -0,0 +1,965 @@ +# 星云链的并行交易分析 + +## 一、并行交易的分类 +
+在当前主流的分片技术中,有网络分片,交易分片和状态分片。网络分片,以太坊的casper(正在开发中),状态分片目前是公认最难也最麻烦的。虽然有一些链做了有益的尝试(如trias等),但整体来说还处于研究中。只有交易分片相对来说比较容易,在老的EOS版本中,也使用过交易分片,而星云链中,则实现了一种交易分片的机制。 +
+交易分片的难点在于处理交易的关系,或者说判断交易的关联性。而区块链的传输的性能和全节点备份机制导致不同节点间对交易处理状态有较大的不同,这也是导致交易分片不容易处理的一个原因。 +
+在星云链中,采用交易的版本比较等方法来实现了一个交易并行的方法。其下将对其源码进行分析,并针对其中的一些具体的问题进行展开描述。 +
+ +## 二、星云链的并行交易 +
+星云链的并行交易在出块的时间进行,当dpos决定产生一个块的时候,会调用交易收集器来决定打包入块的相关的交易。在这其中,为提高速度,使用了多协程并行处理交易的方式。 +
+ +### 1、类图 +
+ +![parallel](img/class-parallel.png) +
+ +### 2、源码分析 +
+从出块开始看下面的代码 +
+ +``` go +//出块代码 +func (dpos *Dpos) newBlock(tail *core.Block, consensusState state.ConsensusState, deadlineInMs int64) (*core.Block, error) { +...... + + block.WorldState().SetConsensusState(consensusState) + block.SetTimestamp(consensusState.TimeStamp()) + block.CollectTransactions(deadlineInMs) +...... + + return block, nil +} +//交易并行代码 +// CollectTransactions and add them to block. +func (block *Block) CollectTransactions(deadlineInMs int64) { + metricsBlockPackTxTime.Update(0) + if block.sealed { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + }).Fatal("Sealed block can't be changed.") + } + + secondInMs := int64(1000) + elapseInMs := deadlineInMs - time.Now().Unix()*secondInMs + logging.VLog().WithFields(logrus.Fields{ + "elapse": elapseInMs, + }).Info("Time to pack txs.") + metricsBlockPackTxTime.Update(elapseInMs) + if elapseInMs <= 0 { + return + } + deadlineTimer := time.NewTimer(time.Duration(elapseInMs) * time.Millisecond) + + pool := block.txPool + + packed := int64(0) + unpacked := int64(0) + + dag := dag.NewDag() + transactions := []*Transaction{} + fromBlacklist := new(sync.Map) + toBlacklist := new(sync.Map) + + // parallelCh is used as access tokens here + parallelCh := make(chan bool, ParallelNum) + // mergeCh is used as lock here + mergeCh := make(chan bool, 1) + over := false + + try := 0 + fetch := 0 + failed := 0 + conflict := 0 + expired := 0 + bucket := len(block.txPool.all) + packing := int64(0) + prepare := int64(0) + execute := int64(0) + update := int64(0) + parallel := 0 + beginAt := time.Now().UnixNano() + + go func() { + for { + mergeCh <- true // lock + if over { + <-mergeCh // unlock + return + } + try++ + tx := pool.PopWithBlacklist(fromBlacklist, toBlacklist) + if tx == nil { + <-mergeCh // unlock + continue + } + + logging.VLog().WithFields(logrus.Fields{ + "tx.hash": tx.hash, + }).Debug("Pop tx.") + + fetch++ + fromBlacklist.Store(tx.from.address.Hex(), true) + fromBlacklist.Store(tx.to.address.Hex(), true) + toBlacklist.Store(tx.from.address.Hex(), true) + toBlacklist.Store(tx.to.address.Hex(), true) + <-mergeCh // lock + + parallelCh <- true // fetch access token + go func() { + parallel++ + startAt := time.Now().UnixNano() + defer func() { + endAt := time.Now().UnixNano() + packing += endAt - startAt + <-parallelCh // release access token + }() + + // step1. prepare execution environment + mergeCh <- true // lock + if over { + expired++ + <-mergeCh // unlock + if err := pool.Push(tx); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to giveback the tx.") + } + return + } + + prepareAt := time.Now().UnixNano() + txWorldState, err := block.WorldState().Prepare(tx.Hash().String()) + preparedAt := time.Now().UnixNano() + prepare += preparedAt - prepareAt + if err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to prepare tx.") + failed++ + + if err := pool.Push(tx); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to giveback the tx.") + } + + fromBlacklist.Delete(tx.from.address.Hex()) + fromBlacklist.Delete(tx.to.address.Hex()) + toBlacklist.Delete(tx.from.address.Hex()) + toBlacklist.Delete(tx.to.address.Hex()) + <-mergeCh // unlock + return + } + <-mergeCh // unlock + + defer func() { + if err := txWorldState.Close(); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to close tx.") + } + }() + + // step2. execute tx. + executeAt := time.Now().UnixNano() + giveback, err := block.ExecuteTransaction(tx, txWorldState) + executedAt := time.Now().UnixNano() + execute += executedAt - executeAt + if err != nil { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "err": err, + "giveback": giveback, + }).Debug("invalid tx.") + unpacked++ + failed++ + + /* if err := txWorldState.Close(); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Debug("Failed to close tx.") + } */ + + if giveback { + if err := pool.Push(tx); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to giveback the tx.") + } + } + if err == ErrLargeTransactionNonce { + // as for the transactions from a same account + // we will pop them out of transaction pool order by nonce ascend + // thus, when we find a transaction with a very large nonce + // we won't try to pack other transactions from the same account in the block + // the account will be in our from blacklist util the block is sealed + if !byteutils.Equal(tx.to.address, tx.from.address) { + fromBlacklist.Delete(tx.to.address.Hex()) + } + toBlacklist.Delete(tx.to.address.Hex()) + } else { + fromBlacklist.Delete(tx.from.address.Hex()) + fromBlacklist.Delete(tx.to.address.Hex()) + toBlacklist.Delete(tx.from.address.Hex()) + toBlacklist.Delete(tx.to.address.Hex()) + } + return + } + + // step3. check & update tx + mergeCh <- true // lock + if over { + expired++ + <-mergeCh // unlock + if err := pool.Push(tx); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to giveback the tx.") + } + return + } + updateAt := time.Now().UnixNano() + dependency, err := txWorldState.CheckAndUpdate() + updatedAt := time.Now().UnixNano() + update += updatedAt - updateAt + if err != nil { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "err": err, + "giveback": giveback, + "dependency": dependency, + }).Info("CheckAndUpdate invalid tx.") + unpacked++ + conflict++ + + if err := pool.Push(tx); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "block": block, + "tx": tx, + "err": err, + }).Info("Failed to giveback the tx.") + } + + fromBlacklist.Delete(tx.from.address.Hex()) + fromBlacklist.Delete(tx.to.address.Hex()) + toBlacklist.Delete(tx.from.address.Hex()) + toBlacklist.Delete(tx.to.address.Hex()) + + <-mergeCh // unlock + return + } + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + }).Debug("packed tx.") + packed++ + + transactions = append(transactions, tx) + txid := tx.Hash().String() + dag.AddNode(txid) + for _, node := range dependency { + dag.AddEdge(node, txid) + } + fromBlacklist.Delete(tx.from.address.Hex()) + fromBlacklist.Delete(tx.to.address.Hex()) + toBlacklist.Delete(tx.from.address.Hex()) + toBlacklist.Delete(tx.to.address.Hex()) + + <-mergeCh // unlock + return + }() + + if over { + return + } + } + }() + + <-deadlineTimer.C + mergeCh <- true // lock + over = true + block.transactions = transactions + block.dependency = dag + <-mergeCh // unlock + + overAt := time.Now().UnixNano() + size := int64(len(block.transactions)) + if size == 0 { + size = 1 + } + averPacking := packing / size + averPrepare := prepare / size + averExecute := execute / size + averUpdate := update / size + + logging.VLog().WithFields(logrus.Fields{ + "try": try, +...... + "dag": block.dependency, + }).Info("CollectTransactions") +} +``` +
+从上面的代码可以看出,选择交易前先处理各种参数,包括时间的更新,并行锁的生成,特别是dag的生成,它是用来判断交易的关系的一个重要的方式,弹出交易数据。这些相关参数配置完成后,启动一个协程。协程内使用一个无限循环来处理交易,在正式选择交易前要先先处理锁的状态和相关的数据的保存,更新数据抓取计算器。 +
+再次启动一个协程,更新并行引用计数器。处理启动时间和异常机制。然后就真正开始选择入块的交易了。 +
+交易交发的代码共分成了三步,一步一步的进行分析。 +
+1)第一步:准备执行环境 +
+ +``` go + +//world_state.go +func (s *states) Prepare(txid interface{}) (*states, error) { + //MVCCDB版本控制准备,见下面代码分析 + changelog, err := s.changelog.Prepare(txid) + if err != nil { + return nil, err + } + + //状态数据库准备 + stateDB, err := s.stateDB.Prepare(txid) + if err != nil { + return nil, err + } + + // Flush all changes in world state into merkle trie + // make a snapshot of world state + //更新世界状态到MPT同时创建一个世界状态的快照 + if err := s.Flush(); err != nil { + return nil, err + } + + //快照 + //创建一个新的帐户状态--会把依赖的相关TID写入新的交易映射 + accState, err := NewAccountState(s.AccountsRoot(), stateDB) + if err != nil { + return nil, err + } + //新的Tx Root MPT状态-同上 + txsState, err := trie.NewTrie(s.TxsRoot(), stateDB, true) + if err != nil { + return nil, err + } + //新的event root mpt + eventsState, err := trie.NewTrie(s.EventsRoot(), stateDB, true) + if err != nil { + return nil, err + } + //新的共识状态 + consensusState, err := s.consensus.NewState(s.ConsensusRoot(), stateDB, true) + if err != nil { + return nil, err + } + + //返回数据结构 + return &states{ + accState: accState, + txsState: txsState, + eventsState: eventsState, + consensusState: consensusState, + + consensus: s.consensus, + changelog: changelog, + stateDB: stateDB, + innerDB: s.innerDB, + txid: txid, + + gasConsumed: make(map[string]*util.Uint128), + events: make(map[string][]*Event), + }, nil +} + +//mvccdb.go +// Prepare a nested transaction这里返回已有的依赖的TID,后面还会在完成后 +//更新状态时更新依赖部分 +func (db *MVCCDB) Prepare(tid interface{}) (*MVCCDB, error) { + db.mutex.Lock() + defer db.mutex.Unlock() + + //判断交易的状态 + if !db.isInTransaction { + return nil, ErrTransactionNotStarted + } + + if tid == nil { + return nil, ErrTidIsNil + } + + if db.preparedDBs[tid] != nil { + return nil, ErrTidIsExist + } + + //数据库中间状态数据的存储准备--与Tid相关关联 + preparedStagingTable, err := db.stagingTable.Prepare(tid) + if err != nil { + return nil, err + } + + //返回MVCCDB实例 + preparedDB := &MVCCDB{ + tid: tid, + storage: db.storage, + stagingTable: preparedStagingTable, + parentDB: db, + isInTransaction: true, + isPreparedDB: true, + isPreparedDBClosed: false, + preparedDBs: make(map[interface{}]*MVCCDB), + isTrieSameKeyCompatibility: db.isTrieSameKeyCompatibility, + } + + db.preparedDBs[tid] = preparedDB + return preparedDB, nil +} + +``` +
+2) 第二步,执行Tx +
+ +``` go +func (block *Block) ExecuteTransaction(tx *Transaction, ws WorldState) (bool, error) { + //检查交易是否合规 + if giveback, err := CheckTransaction(tx, ws); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "err": err, + }).Info("Failed to check transaction") + return giveback, err + } + //检验执行 + if giveback, err := VerifyExecution(tx, block, ws); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "err": err, + }).Info("Failed to verify transaction execution") + return giveback, err + } + + //接受交易 + if giveback, err := AcceptTransaction(tx, ws); err != nil { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "err": err, + }).Info("Failed to accept transaction") + return giveback, err + } + + return false, nil +} +// CheckTransaction in a tx world state +func CheckTransaction(tx *Transaction, ws WorldState) (bool, error) { + // check nonce + fromAcc, err := ws.GetOrCreateUserAccount(tx.from.address) + if err != nil { + return true, err + } + + // pass current Nonce.检测随机值 + currentNonce := fromAcc.Nonce() + + if tx.nonce < currentNonce+1 { + // Nonce is too small, won't giveback the tx + return false, ErrSmallTransactionNonce + } else if tx.nonce > currentNonce+1 { + return true, ErrLargeTransactionNonce + } + + return false, nil +} +//检测帐户状态 +func (as *accountState) GetOrCreateUserAccount(addr byteutils.Hash) (Account, error) { + acc, err := as.getAccount(addr) + if err != nil && err != ErrAccountNotFound { + return nil, err + } + if err == ErrAccountNotFound { + acc, err = as.newAccount(addr, nil, nil) + if err != nil { + return nil, err + } + return acc, nil + } + return acc, nil +} +//在MVCCDB中记录相关帐户 +func (s *states) recordAccount(acc Account) (Account, error) { + if err := s.changelog.Put(acc.Address(), acc.Address()); err != nil { + return nil, err + } + return acc, nil +} +``` +
+看一下校验执行,这个比较麻烦: +
+ +``` go +// VerifyExecution transaction and return result. +//校验执行交易并返回结果--这个没有太特别的,除了和世界状态的比较 +func VerifyExecution(tx *Transaction, block *Block, ws WorldState) (bool, error) { + // step0. perpare accounts.上文解析过,查看帐户状态 + fromAcc, err := ws.GetOrCreateUserAccount(tx.from.address) + if err != nil { + return true, err + } + //同理 + toAcc, err := ws.GetOrCreateUserAccount(tx.to.address) + if err != nil { + return true, err + } + + // step1. check balance >= gasLimit * gasPrice + //同以太坊,检测Gas + limitedFee, err := tx.gasLimit.Mul(tx.gasPrice) + if err != nil { + // Gas overflow, won't giveback the tx + return false, ErrGasFeeOverflow + } + if fromAcc.Balance().Cmp(limitedFee) < 0 { + // Balance is smaller than limitedFee, won't giveback the tx + return false, ErrInsufficientBalance + } + + // step2. check gasLimit >= txBaseGas. + //判断块的gasLimit是不是超出 + baseGas, err := tx.GasCountOfTxBase() + if err != nil { + // Gas overflow, won't giveback the tx + return false, ErrGasCntOverflow + } + gasUsed := baseGas + if tx.gasLimit.Cmp(gasUsed) < 0 { + logging.VLog().WithFields(logrus.Fields{ + "error": ErrOutOfGasLimit, + "transaction": tx, + "limit": tx.gasLimit, + "acceptedGas": gasUsed, + }).Debug("Failed to check gasLimit >= txBaseGas.") + // GasLimit is smaller than based tx gas, won't giveback the tx + return false, ErrOutOfGasLimit + } + + // !!!!!!Attention: all txs passed here will be on chain. + //注意:必须是链上交易 + // step3. check payload vaild.交易内容的检测 + payload, payloadErr := tx.LoadPayload() + if payloadErr != nil { + return submitTx(tx, block, ws, gasUsed, payloadErr, "Failed to load payload.", "") + } + + // step4. calculate base gas of payload + payloadGas, err := gasUsed.Add(payload.BaseGasCount()) + if err != nil { + logging.VLog().WithFields(logrus.Fields{ + "err": err, + "tx": tx, + "gasUsed": gasUsed, + "payloadBaseGas": payload.BaseGasCount(), + "block": block, + }).Error("Failed to add payload base gas, unexpected error") + metricsUnexpectedBehavior.Update(1) + return submitTx(tx, block, ws, gasUsed, ErrGasCntOverflow, "Failed to add the count of base payload gas", "") + } + gasUsed = payloadGas + if tx.gasLimit.Cmp(gasUsed) < 0 { + return submitTx(tx, block, ws, tx.gasLimit, ErrOutOfGasLimit, "Failed to check gasLimit >= txBaseGas + payloasBaseGas.", "") + } + + // step5. check balance >= limitedFee + value. and transfer + //帐户是否满足需求 + minBalanceRequired, balanceErr := limitedFee.Add(tx.value) + if balanceErr != nil { + return submitTx(tx, block, ws, gasUsed, ErrGasFeeOverflow, "Failed to add tx.value", "") + } + if fromAcc.Balance().Cmp(minBalanceRequired) < 0 { + return submitTx(tx, block, ws, gasUsed, ErrInsufficientBalance, "Failed to check balance >= gasLimit * gasPrice + value", "") + } + var transferSubErr, transferAddErr error + transferSubErr = fromAcc.SubBalance(tx.value) + if transferSubErr == nil { + transferAddErr = toAcc.AddBalance(tx.value) + } + if transferSubErr != nil || transferAddErr != nil { + logging.VLog().WithFields(logrus.Fields{ + "subErr": transferSubErr, + "addErr": transferAddErr, + "tx": tx, + "fromBalance": fromAcc.Balance(), + "toBalance": toAcc.Balance(), + "block": block, + }).Error("Failed to transfer value, unexpected error") + metricsUnexpectedBehavior.Update(1) + return submitTx(tx, block, ws, gasUsed, ErrInvalidTransfer, "Failed to transfer tx.value", "") + } + + // step6. calculate contract's limited gas + contractLimitedGas, err := tx.gasLimit.Sub(gasUsed) + if err != nil { + logging.VLog().WithFields(logrus.Fields{ + "err": err, + "tx": tx, + "gasUsed": gasUsed, + "block": block, + }).Error("Failed to calculate payload's limit gas, unexpected error") + metricsUnexpectedBehavior.Update(1) + return submitTx(tx, block, ws, tx.gasLimit, ErrOutOfGasLimit, "Failed to calculate payload's limit gas", "") + } + + // step7. execute contract. + //执行合约 + gasExecution, exeResult, exeErr := payload.Execute(contractLimitedGas, tx, block, ws) + if exeErr == ErrUnexpected { + return false, exeErr + } + + // step8. calculate final gas. + allGas, gasErr := gasUsed.Add(gasExecution) + if gasErr != nil { + return submitTx(tx, block, ws, gasUsed, ErrGasCntOverflow, "Failed to add the fee of execution gas", "") + } + if tx.gasLimit.Cmp(allGas) < 0 { + return submitTx(tx, block, ws, tx.gasLimit, ErrOutOfGasLimit, "Failed to check gasLimit >= allGas", "") + } + + // step9. over完成 + return submitTx(tx, block, ws, allGas, exeErr, "Failed to execute payload", exeResult) +} +``` +
+再看一下接受交易: +
+ +``` go +func AcceptTransaction(tx *Transaction, ws WorldState) (bool, error) { + // record tx 转成原始交易 + pbTx, err := tx.ToProto() + if err != nil { + return true, err + } + //解析成字节流 + txBytes, err := proto.Marshal(pbTx) + if err != nil { + return true, err + } + //保存交易 + if err := ws.PutTx(tx.hash, txBytes); err != nil { + return true, err + } + // incre nonce计算随机值 + fromAcc, err := ws.GetOrCreateUserAccount(tx.from.address) + if err != nil { + return true, err + } + //nonce +1 + fromAcc.IncrNonce() + // No error, won't giveback the tx + return false, nil +} +``` +
+完成上述动作后,就可以在交易池中增加Tx了。 +
+3) 第三步:检验并更新MVCCDB +
+仍然是先要控制一下锁,然后开始工作: +
+ +``` go +// Push tx into pool +func (pool *TransactionPool) Push(tx *Transaction) error { + pool.mu.Lock() + defer pool.mu.Unlock() + // add tx log in super node + if pool.bc.superNode == true { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + }).Debug("Push tx to transaction pool") + } + + //下面分两处理不支持的Tx的异常 + //if is super node and tx type is deploy, do unsupported keyword checking. + if pool.bc.superNode == true && len(pool.bc.unsupportedKeyword) > 0 && len(tx.Data()) > 0 { + if tx.Type() == TxPayloadDeployType { + data := string(tx.Data()) + keywords := strings.Split(pool.bc.unsupportedKeyword, ",") + for _, keyword := range keywords { + keyword = strings.ToLower(keyword) + if strings.Contains(data, keyword) { + logging.VLog().WithFields(logrus.Fields{ + "tx.hash": tx.hash, + "unsupportedKeyword": keyword, + }).Debug("transaction data has unsupported keyword") + unsupportedKeywordError := fmt.Sprintf("transaction data has unsupported keyword(keyword: %s)", keyword) + return errors.New(unsupportedKeywordError) + } + } + } + } + + //if is super node and tx type is deploy, do unsupported keyword checking. + if pool.bc.superNode == true && len(pool.bc.unsupportedKeyword) > 0 && len(tx.Data()) > 0 { + if tx.Type() == TxPayloadDeployType { + data := string(tx.Data()) + keywords := strings.Split(pool.bc.unsupportedKeyword, ",") + for _, keyword := range keywords { + keyword = strings.ToLower(keyword) + if strings.Contains(data, keyword) { + logging.VLog().WithFields(logrus.Fields{ + "tx": tx, + "unsupportedKeyword": keyword, + }).Debug("transaction data has unsupported keyword") + return ErrUnsupportedKeyword + } + } + } + } + // verify non-dup tx重复判断 + if _, ok := pool.all[tx.hash.Hex()]; ok { + metricsDuplicateTx.Inc(1) + return ErrDuplicatedTransaction + } // ToRefine: refine the lock scope + + // if tx's gasPrice below the pool config lowest gasPrice, return ErrBelowGasPrice + if tx.gasPrice.Cmp(pool.minGasPrice) < 0 { + metricsTxPoolBelowGasPrice.Inc(1) + return ErrBelowGasPrice + } + + if tx.gasLimit.Cmp(util.NewUint128()) <= 0 { + metricsTxPoolGasLimitLessOrEqualToZero.Inc(1) + return ErrGasLimitLessOrEqualToZero + } + + if tx.gasLimit.Cmp(pool.maxGasLimit) > 0 { + metricsTxPoolOutOfGasLimit.Inc(1) + return ErrOutOfGasLimit + } + + // verify hash & sign of tx + if err := tx.VerifyIntegrity(pool.bc.chainID); err != nil { + metricsInvalidTx.Inc(1) + return err + } + + // cache the verified tx + pool.pushTx(tx) + // drop max tx in longest bucket if full + //如果超过极值就把最长时间的删除 + if len(pool.all) > pool.size { + poollen := len(pool.all) + pool.dropTx() + + logging.VLog().WithFields(logrus.Fields{ + "tx": tx.StringWithoutData(), + "size": pool.size, + "bpoolsize": poollen, + "apoolsize": len(pool.all), + "bucketsize": len(pool.buckets), + }).Debug("drop tx") + } + + // trigger pending transaction + event := &state.Event{ + Topic: TopicPendingTransaction, + Data: tx.JSONString(), + } + pool.eventEmitter.Trigger(event) + + return nil +} +``` +
+检测并更新状态: +
+ +``` go +func (tws *txWorldState) CheckAndUpdate() ([]interface{}, error) { + dependencies, err := tws.states.CheckAndUpdateTo(tws.parent.states) + if err != nil { + return nil, err + } + tws.parent = nil + return dependencies, nil +} +func (s *states) CheckAndUpdateTo(parent *states) ([]interface{}, error) { + dependency, err := s.changelog.CheckAndUpdate() + if err != nil { + return nil, err + } + _, err = s.stateDB.CheckAndUpdate() + if err != nil { + return nil, err + } + if err := parent.Replay(s); err != nil { + return nil, err + } + return dependency, nil +} +//MVCCDB的检测和更新--去年让他日志和MPT的更新 +//dependency, err := s.changelog.CheckAndUpdate() +//_, err = s.stateDB.CheckAndUpdate() +// CheckAndUpdate merge current changes to `FinalVersionizedValues`. +func (db *MVCCDB) CheckAndUpdate() ([]interface{}, error) { + db.mutex.Lock() + defer db.mutex.Unlock() + + if !db.isInTransaction { + return nil, ErrTransactionNotStarted + } + + if !db.isPreparedDB { + return nil, ErrDisallowedCallingInNoPreparedDB + } + + if db.isPreparedDBClosed { + return nil, ErrPreparedDBIsClosed + } + + ret, err := db.stagingTable.MergeToParent() + + if err == nil { + // cleanup. + db.stagingTable.Purge() + } + + return ret, err +} +// MergeToParent merge key/value pair of tid to `finalVersionizedValues` which the version of value are the same. +//关键部分:通过版本比较来合并相关的值,看上面的中间变量设置 +func (tbl *StagingTable) MergeToParent() ([]interface{}, error) { + if tbl.parentStagingTable == nil { + return nil, ErrParentStagingTableIsNil + } + + tbl.parentStagingTable.mutex.Lock() + defer tbl.parentStagingTable.mutex.Unlock() + + tbl.mutex.Lock() + defer tbl.mutex.Unlock() + + dependentTids := make(map[interface{}]bool) + conflictKeys := make(map[string]interface{}) + + // 1. check version. + targetValues := tbl.parentStagingTable.versionizedValues + + for keyStr, fromValueItem := range tbl.versionizedValues { + targetValueItem := targetValues[keyStr] + + if targetValueItem == nil { + continue + } + + // 1. record conflict.标记冲突数据 + //标准是版本号和相同的KEY + if fromValueItem.isConflict(targetValueItem, tbl.isTrieSameKeyCompatibility) { + conflictKeys[keyStr] = targetValueItem.tid + continue + } + + // 2. record dependentTids. + //标记依赖tid + // skip default value loaded from storage. + //忽略默认值 + if targetValueItem.isDefault() { + continue + } + + // ignore same parent tid for dependentTids. + //忽略同父路径操作 + if targetValueItem.tid == tbl.parentStagingTable.tid { + continue + } + + // ignore version check when TrieSameKeyCompatibility is enabled. + if tbl.isTrieSameKeyCompatibility { + continue + } + + dependentTids[targetValueItem.tid] = true + } + + if len(conflictKeys) > 0 { + logging.VLog().WithFields(logrus.Fields{ + "tid": tbl.tid, + "parentTid": tbl.parentStagingTable.tid, + "conflictKeys": conflictKeys, + }).Debug("Failed to be merged into parent.") + return nil, ErrStagingTableKeyConfliction + } + + // 2. merge to final. + //合并 + // incr parentStagingTable.globalVersion.处理合并后的版本号 + tbl.parentStagingTable.globalVersion++ + + for keyStr, fromValueItem := range tbl.versionizedValues { + // ignore default value item. + if fromValueItem.isDefault() { + continue + } + + // ignore non-dirty. + if !fromValueItem.dirty { + continue + } + + // merge. + value := fromValueItem.CloneForMerge(tbl.parentStagingTable.globalVersion) + targetValues[keyStr] = value + } + + //遍历增加新的tid + tids := make([]interface{}, 0, len(dependentTids)) + for key := range dependentTids { + tids = append(tids, key) + } + + return tids, nil +} +``` +
+再看最后: +
+ +``` go +txid := tx.Hash().String() +dag.AddNode(txid) +for _, node := range dependency { + dag.AddEdge(node, txid) +} +``` +
+将节点放入dag中,dag的特点是快速,但不容易回溯。 +
+通过上面的源码分析,就可以清晰的看到星云链的交易并行其实思路还是比较清晰的,主要原理是利用MVCCDB记录的版本号,在MPT状态树中存储着相关的依赖的tid,在协程中执行交易时,会对其进行判断,如果交易不符合情况,就会返回错误,然后利用内存数据进行回滚。如果成功,则更新新的世界状态和相关的数据、日志。 +
+ +## 三、总结 +星云链从一个方向上解决了交易分片的过程,但是看起来还是略微有些简单粗暴,从源码分析来看,还是可能会产生很多的冗余操作,但这也算是对交易分片的一个非常有益的探索。 +
diff --git "a/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\346\225\264\344\275\223\346\236\266\346\236\204.md" "b/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\346\225\264\344\275\223\346\236\266\346\236\204.md" new file mode 100644 index 0000000..e8d3b1e --- /dev/null +++ "b/nebulas/\346\230\237\344\272\221\351\223\276\347\232\204\346\225\264\344\275\223\346\236\266\346\236\204.md" @@ -0,0 +1,252 @@ +# 星云链的整体架构 + +
+ +## 一、整体结构 +
+星云链是一条能够量化价值尺度、具备自进化能力,并能促进区块链生态建设的区块链系统。它有以下几个特点: + +### 1、星云指数Nebulas Rank +
+星云指数是定义价值尺度 (NR)的指标 ,通过综合考虑链中各个账户的流动性及传播性,NR 试图为每个账户建一个个可信、可计算及可复现的普适价值尺度刻画。可以预见,在NR 之上,通过挖掘更大纵深的价值,星云链的平台上将会涌现更多、更丰富的应用。 + +### 2、星云原力Nebulas Force(NF) +
+描述区块链系统及应用的进化能力。星云原力做为驱动区块链系统及应用发展的第一推动力,包括三个方面:星云链虚拟机NVM(Nebulas Virtual Machine),区块链系统中核⼼协议的升级,以及运行在区块链系统之上的智能合约的升级。 +目前这块源码完成了一部分,官网的路线图是2019.Q4完成。 + +### 3、开发者激励协议Developer Incentive Protocol(DIP) +
+为了更好地建立区块链应用生态环境,星云链将通过星云币(NAS)来激励为生态助力的优秀应用开发者,促进星云链更加丰富多元的价值沉淀。 +目前这块源码尚未完成,官网的路线图是2018.Q4。 + +### 4、贡献度证明共识算法Proof of Devotion(PoD) +
+星云链提出了共识算法的三个重要指标,即快速、不可逆和公平性,PoD 通过融合PoS 和PoI 的优势,结合星云链中的价值尺度,在保证快速和不可逆的前提下,率先加⼊了公平性的考量。 +目前这块源码未完成,官网的路线图是2019.Q4完成。 + +### 5、去中心化应用的搜索引擎 +
+基于我们所定义的价值尺度,星云链构建了⼀个针对去中⼼化应⽤的搜索引擎,帮助⽤户在海量区块链应用中,找到符合用户期望及应用场景的应用。 +搜索引擎是基于星云指数的,所以目前应该也是规划中。 +它的主要设计架构层次,如下图: +
+ +![overview](img/nebulas-overview.png) +
+ +## 二、代码结构和数据流程 +
+ +### 1、代码结构: +
+ +![code](img/code-m.png) +
+上面的文件夹看起来不少,其实主要分为以下几块: +
+帐户: +
+主要是用来创建操作交易的帐户,主要包括account,keydir,crypto等。 +
+数据库: +
+主要用来区块数据的读写,包括storage,native-lib. +
+命令集: +
+主要用来创建控制台操作和主程序启动等。主要包括cmd等。 +
+网络: +
+星云链的网络通信基础,包括net,rpc,sync等。 +
+服务: +
+控制协调星云链的各个模块的服务操作。主要包括neblet等。 +
+核心和共识: +
+区块链的主要功能和共识部分,主要包括core、consensus、common。 +
+星云原力: +
+星云链的一个重要指标,主要在nf文件夹内,目前虚拟机部分已经初步完成。其它正在进行。 +
+ +### 2、主要的数据流程 + +它的主要数据流程如下图: +
+ +![workflow](img/workflow.png) +
+ +### 3、本地矿工产生一个新区块的流程如下: +
+ +![blockpool](img/blockpool.png) + +## 三、重点模块 +### 1、并行交易 +
+星云链的并发执行交易模型如下: +
+ +![multhread](img/parallel.png) +
+为了执行并发,星云链在产生区块时进行了交易的收集验证动作,其使用协程来完成: +
+ +``` go +consensus/dpos.go +func (dpos *Dpos) newBlock(tail *core.Block, consensusState state.ConsensusState, deadlineInMs int64) (*core.Block, error) { + ……. + block.CollectTransactions(deadlineInMs) + …… + + return block, nil +} +其它调用: +core/block.go +// CollectTransactions and add them to block. +func (block *Block) CollectTransactions(deadlineInMs int64) { + metricsBlockPackTxTime.Update(0) +……. + + pool := block.txPool + ………. + + go func() { + for { +…… +//弹出GAS最高交易 + tx := pool.PopWithBlacklist(fromBlacklist, toBlacklist) +……. + + fetch++ + fromBlacklist.Store(tx.from.address.Hex(), true) + fromBlacklist.Store(tx.to.address.Hex(), true) + toBlacklist.Store(tx.from.address.Hex(), true) + toBlacklist.Store(tx.to.address.Hex(), true) + <-mergeCh // lock + + parallelCh <- true // fetch access token + go func() { + parallel++ + startAt := time.Now().UnixNano() + defer func() { + endAt := time.Now().UnixNano() + packing += endAt - startAt + <-parallelCh // release access token + }() + + // step1. prepare execution environment + mergeCh <- true // lock + if over { + expired++ + <-mergeCh // unlock + if err := pool.Push(tx); err != nil { + ……. + } + return + } + + prepareAt := time.Now().UnixNano() + txWorldState, err := block.WorldState().Prepare(tx.Hash().String()) + preparedAt := time.Now().UnixNano() + prepare += preparedAt – prepareAt + …… + <-mergeCh // unlock + …… + + // step2. execute tx. + executeAt := time.Now().UnixNano() + //此处非常重要,各种验证和合约的执行都在此函数中调用 + giveback, err := block.ExecuteTransaction(tx, txWorldState) + executedAt := time.Now().UnixNano() + execute += executedAt - executeAt + …… + + // step3. check & update tx + mergeCh <- true // lock + ……. + updateAt := time.Now().UnixNano() + //检查更新状态参数 + dependency, err := txWorldState.CheckAndUpdate() + updatedAt := time.Now().UnixNano() + update += updatedAt - updateAt + ……. + packed++ + //添加交易 + transactions = append(transactions, tx) + txid := tx.Hash().String() + //添加节点到DAG + dag.AddNode(txid) + for _, node := range dependency { + //添加到DAG中 + dag.AddEdge(node, txid) + } + ……. + + <-mergeCh // unlock + return + }() + …… + } + }() + + <-deadlineTimer.C + mergeCh <- true // lock + over = true + block.transactions = transactions + block.dependency = dag + <-mergeCh // unlock +…… +} + +``` +
+ +### 2、块池 +
+在星云链中,目前使用DPOS通过引进伪随机来产生出块者,而设计中的POD目前尚未完成,可能是出于设计的需要一些相关的基础架构都按照POD的方式来完成。 +
+其中朝代和epoch等相关都已经设计完成,所以通过一个blockpool来处理相关的设计要求。这也算是星云链比较独特的一个方面。 +朝代更替: +
+ + ![dynasty](img/dynasty.png) +
+上链的区块判断: +
+ +![judge](img/blockjudge.png) +
+ +### 3、版本比较 +为了保证交易等的事务并发控制,引进了版本控制数据库,MVCC支持嵌套的事务。 + +## 四、虚拟机引擎 +
+虚拟机的引擎目前采用的是以LLVM为核心组件(正在开发中),目前使用V8引擎来实现对智能合约的支持。V8引擎的优点就在于对前端开发者的相对友好易用。学习成本低,更利用智能合约的推广。 +LLVM其运行机制如下: +
+ +![runtime](img/runtime.png) +
+V8运行机制如下图: +
+ +![v8](img/runtimeV8.png) +
+目前星云链支持JavaScript或TypeScript两种语言。在星云链的智能合约中必须包含一个init函数,它只有在部署本合约时执行。 +如果函数是以下划线_起始,则说明此函数为私有。 + +## 五、总结 +
+整体来看,星云链的整体目标完成度还是比较少的。虽然目前可以运行,但是几个重要的核心部分或者说突出特点,都尚未完全完成。 +
+其提出的星云原力和星云指数以及通过它们发展出来的搜索引擎,有着不同于其它链的鲜明的特点。特别是链内交易并行,EOS在原来的版本也曾经规划过,但真正实现还是在星云链上看到,具有很强的借鉴性。 + +
diff --git a/p2p/design.md b/p2p/design.md new file mode 100644 index 0000000..cad7eeb --- /dev/null +++ b/p2p/design.md @@ -0,0 +1,245 @@ +## p2p的架构设计及部分实现细节 +
+ +#### 一 前言 +##### 1 工作方式 +p2p模块总是在区块链的众多子模块中优先启动,它承担将本地消息向外发送,获取外部消息的职责;获取与发送消息之前自身又需要维持一个最低的链接数,维持这个最低链接数就需要获取跟多节点信息。 +##### 2 本文介绍 +与区块链的多个子模块交互、消息传输的隐私性与安全性、去中心化等一系列问题,使得设计一个p2p模块自然会涉及到很多问题, 然而很多优秀的公链已经为我们提供了一些参考的实例。 本文分析主流公链的的p2p模块及实现的一些细节问题,但是限于篇幅的原因仅会给出部分具体实例。 +
+ +#### 二 架构分析及实现细节 + 从职责划分上来讲, p2p模块可分为两块:_发现服务_、_消息传输服务_。发现服务主要用来获取更多节点信息,使得消息传输服务在与其他节点建立链接时有更多的选择, 节点信息包括IP、端口、协议、节点ID等,链接到更多的节点意味着在以下方面有更大概率:更新的本地状态、同等算力下更多的节点收到本地挖出的block。消息传输服务,顾名思义,就是用来传输消息的,比如共识消息、状态同步消息、心跳包、tx/block广播消息等。 消息传输服务从业务深度上可划分为: + + 链接资源申请管理 + + 协议/代码版本检测 + + 隐私保护 + + 消息路由 + + 链接资源释放管理 + +##### 1 发现服务 +主流公链的发现服务一般有三个组成部分: + + 节点信息的持久化与查询 + + 引擎部分:维护链接池、调度节点发现 + + 节点发现, 主要使用pex、dht算法,概括的来讲: + - pex是在已建立链接的俩个节点之间交换各自已知节点信息 + - dht解决了p2p网络中心化tracker的问题 + +###### 1.1 节点信息与链接池维护 +节点信息的应该区分为: ++ 已经建立过链接的节点 ++ 未建立过链接的节点,通过节点发现获得 + +未建立过链接的节点应该额外的保存两条信息 + + 最后一次尝试链接的时间, 用于控制尝试链接的间隔时间 + + 尝试的次数,次数超过一定数量,该信息应该被删除 + +重启后应该优先尝试已经建立链接的节点。 go-ipfs采用bitswap算法, 该算法通过负债率r调整消息发送概率P, 负债率越大消息被丢弃的概率越大。通过累计发送和接受到的消息字节数计算负债率, 进一步调整远程节点的消息被处理的概率,所以我们也建议将这这俩个指标持久化, 供消息模块使用以及尝试链接的优先级。尝试失败超过一定的数量后, 可以认为这条信息已经失效了。 + +```golang +计算负债率、丢弃概率公式如下: +r = bytes_sent / (bytes_recv + 1) +P = 1− 1/(1+exp(6−3r)) +``` +###### 1.2 节点发现 +pex算法本身很简单, 我们不做的过多的赘述。kademlia作为目前dht的主流实现,其算法主体思想如下: + + 1. A节点广播一个(hash, entry)的键值对, 一般为(节点ID, NetworkAddr) + 2. B将(hash, entry)加入本地table + 3. B使用table提供基于距离关系的查找 + +table的结构为[距离]list, len(list) < 20(建议), 将(hash, entry)加入table的大致过程如下, 可以一定程度的抵抗女巫攻击 +```golang +idx := 计算与本节点ID的距离 +table[idx] = append(table[idx], entry) +backup(table[20:]) // 作为备份信息 +table[idx] = table[idx][:20] +``` +计算距离的算法如下: +```golang +func prefixLength(xor uint8) uint { + switch { + case xor == 1: + return 7 + case xor >= 2 && xor <= 3: + return 6 + case xor >= 4 && xor <= 7: + return 5 + case xor >= 8 && xor <= 15: + return 4 + case xor >= 16 && xor <= 31: + return 3 + case xor >= 32 && xor <= 63: + return 2 + case xor >= 64 && xor <= 127: + return 1 + case xor >= 128 && xor <= 255: + return 0 + } + return 8 +} + +func calcDistance(a, b []byte) uint { + c := uint(0) + for i := 0; i < len(a) && i < len(b); i++ { + x := a[i] ^ b[i] + if x == 0 { + c += 8 + } else { + c += prefixLength(x) + break + } + } + return uint(len(a))*8 - c + +} +``` + +
+ +##### 2 消息传输服务 +###### 2.1 链接资源申请管理 +p2p的消息传输服务应该不仅作为server通过监听端口处理链接请求, 还需作为client发起链接, 俩个方向的链接都应通过令牌的方式控制连接数。仅作为server处理链接会降低攻击难度;仅作为client发起链接会失去大量链接的机会,因为其他节点发起与本节点的链接失败后会抛弃本节点信息。此过程有俩个细节需要说明: ++ 节点之间保持一个链接即可,不要漏过处于dialing状态的节点 ++ 需要标识是链接的发起方还是监听方,原因是: + - 后续的心跳等模块可能需要区分, 发起方作为client需要维持心跳 + - 释放链接是需要依据标识归还不同的令牌 + +为了保证消息的完整性, ethereum使用如下的消息报文格式来拆解包 +```golang +| length| msgHash | msg | +``` + +###### 2.2 协议/代码版本检测 +在具体的业务逻辑开始前进行本代码版本检测可以实现控制代码升级、分叉,进行协议检测可以避免大量无效链接。目前ethereum的节点分为俩种full node、light node, 即将到来的sharding版本又增加了更多的节点角色类型, light node仅同步header信息以及向full node请求merkel proof, 而full node是否支持为light node提供服务就需要在这一步确认。 + +###### 2.3 隐私保护 +区块链建立在非对称加密算法ECC上,非对称加密算法做加解密效率很低,此外ECC是无法用来加解密信息只是用来’验证‘ , 所以提供消息隐私性功能需要借助对称加密算法比如AES算法。一般使用STS实现AES密钥同步, 算法如下 + +```golang +concat := func(a, b uint)uint{ + v, _ := strconv.Atoi(fmt.Spintf("%d%d", a, b)) + return uint(v) +} +mod := func(x, y uint)uint{ + return x % y +} +pow := func(x, y uint)uint{ + return uint(math.Pow(x, y)) +} +算法: +A, B : set p = 23, g = 5 +A : generate a = 6, set va=mod(concat(g, a), p)=8 , sendTo(B, 8) // 56 % 23 == 8 +B : generate b = 15, set vb=mod(concat(g, b), p)=19 , sendTo(A, 19) // 515 % 23 == 19 +A : set secret=mod(pow(vb, a), p)=2 // 47045881 % 23=19 +B : set secret=mod(pow(va, b), p)=2 // 35184372088832% 23 = 2 + +密钥安全性保证证明如下: + +----------节点A-------------+-------节点B----------+---恶意节点 -----------+ + + + + + + 已知信息 a,p,g,concat,mod + b, p,g,concat,mod + p,g,concat,mod + + + + + + + 已知信息 vb, a, p, pow, mod + va, b, p, pow, mod + va,vb,p,pow,mod + + + + + + + 已知信息 secret + secret + - + + +-------------------------------------------------------------------------+ +``` +###### 2.4 消息路由 +主流的公链包括若干个模块比如:共识、交易池、账本、P2P等。 共识模块需要通过P2P 广播投票信息、收集投票出块;交易池将验证tx通过p2p 广播给周围节点, 通过p2p收集tx验证后加入本地交易池;账本通过p2p同步账本信息,比如我们可以在心跳包中加入账本高度, 账本模块对比本地高度请求block。 +ethereum的交易池模块业务流程如下: ++ 接收tx (来源包括rpc调用、p2p转发等) ++ tx验证(包括nonce, value、gas、signature)后加入本地交易池, 并依据tx中的nonce删除老旧tx ++ 通过gasprice对交易池里的tx排序 ++ 为出块模块(挖矿、共识)提供tx list ++ 调用p2p广播验证过的tx ++ 监听eventhub的“挖矿”主题,删除本地交易池中已经被出块模块打包进入block的tx ++ 监听eventhub的”新块“主题,删除本地交易池中已经其他节点打包进入block的tx +我们可以清晰的看到p2p与交易池的交互发生:1、5。交易池与p2p调用实现大致如下 +```golang +type Message struct{ + topic string // 消息主题, 此处是 txpool + code uint + payload []byte // 消息体, 解析方式需由各模块自定义 推荐google/proto.Marshal/Unmarshal +} +type Context interface{ + Send(uint32, []byte) // 模块调用p2p向远程节点发送消息 + + ID() string // 节点标识符 + Topic()string + ... +} + +type PeerHandler interface { + NewPeer(Context)TopicHandler + ... +} + +type TopicHandler interface { + Setup()error + Handle(uint32, []byte) error // 远程节点向本节点发消息 + Treardown() +} + +func readLoop(conn Conn){ + for{ + data := conn.Read() // 消息完整性检测、数据加解密在conn封装层完成 + go disptath(data) + ...... + } +} + +var mTopicHandler map[string]TopicHandler + +func dispatch(data []byte){ + topic, code, payload := decode(data) + h, exists := mTopicHandler[topic] // 依据 topic做路由 + if exists{ + h.Handle(code, payload) + } + ...... +} + +type TxPoolServer server{ + txHandlers map[string]txHandler +} + +func (self *TxPoolServer)NewPeer(ctx context)TxPoolHandler{ + h := txHandler{...} + self.txHandlers[ctx.ID()] = h + ...... + return h +} + +func (self TxPoolServer)broadcastTx(tx Transaction, except func(string)bool){ // 广播交易 + payload = encode(tx) + for id, h := range self.txHandlers{ + if except!=nil && except(id){ // 哪些节点不能发 + continue + } + h.Send(1, payload) + } +} + +func (self txHandler)Handle(code uint, msg []byte)error{ + switch code{ + case 1: // new tx + ...... + } + return nil +} + +``` +p2p模块应该对外只承担消息发送与接受、对内实现节点发现的功能, 具体的业务逻辑不应该交由p2p处理,事实上主流公链也都采用如下的架构来解耦具体业务: + + p2p模块负责网络读 + + conn 封装 + + 消息解析及路由 + + handler处理业务 + + 消息拼装 + + p2p模块负责网络写 + +###### 2.5 链接资源释放管理 +链接的client端应该负责发送心跳包维持链接, server端释放不活跃、异常的client; 释放链接的同时应该同步通知与该节点交互的各个子模块。注意前文提到需要‘归还不同的令牌‘。 +
+ +#### 3 总结 +以上只是简单描述了设计几个核心模块时需要主要注意的问题以及几条建议,当然还有一些其他问题比如:流量控制、节点管理、ssl transport、Conn封装、消息去重等,这些问题之所以没有展开是因为它们不参与核心流程, 但是如果设计的时候没有考虑到,那么这个p2p模块也只能算是基本可用。 + diff --git a/polkadot/img/polkadot-role.jpg b/polkadot/img/polkadot-role.jpg new file mode 100644 index 0000000..d624a6a Binary files /dev/null and b/polkadot/img/polkadot-role.jpg differ diff --git a/polkadot/img/polkadot-source.png b/polkadot/img/polkadot-source.png new file mode 100644 index 0000000..cc972dd Binary files /dev/null and b/polkadot/img/polkadot-source.png differ diff --git a/polkadot/img/polkadot-source1.png b/polkadot/img/polkadot-source1.png new file mode 100644 index 0000000..7319d60 Binary files /dev/null and b/polkadot/img/polkadot-source1.png differ diff --git a/polkadot/img/polkadot-source2.png b/polkadot/img/polkadot-source2.png new file mode 100644 index 0000000..5c44b1d Binary files /dev/null and b/polkadot/img/polkadot-source2.png differ diff --git a/polkadot/img/polkadot.jpg b/polkadot/img/polkadot.jpg new file mode 100644 index 0000000..ddb5c44 Binary files /dev/null and b/polkadot/img/polkadot.jpg differ diff --git "a/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\344\273\213\347\273\215\345\222\214\346\272\220\347\240\201\346\250\241\345\235\227.md" "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\344\273\213\347\273\215\345\222\214\346\272\220\347\240\201\346\250\241\345\235\227.md" new file mode 100644 index 0000000..4c226bd --- /dev/null +++ "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\200\344\273\213\347\273\215\345\222\214\346\272\220\347\240\201\346\250\241\345\235\227.md" @@ -0,0 +1,146 @@ +# 波卡链源码分析之一介绍和源码模块 + +## 一、波卡链(polkadot)介绍 +波卡链是一个基于中继链的多链y系统,从它的白皮书来看,应该是非常强大,但从的研发进度来看,确实是有点慢。不过好在github上有演进的源码,就针对这些进行分析,如果有什么新版本对老版本的不同,会在看到后及时指出。 + +## 二、波卡链的架构 +波卡链的设计架构如下图: +
+ +![polkadot](img/polkadot.jpg) + +从上图可以看出,波卡链主要是由中继链为纽带,连接不同的平行公链。以桥接的方式通过队列进行数据(交易)间的传递。跨链的路由是由中继链和其验证人一起实现的核心功能,其管理以下主要逻辑:一个提交的交易(简言之为“提交”)是如何从一个来源(source)平行链的出口被强制地路由到另一个目标(destination)平行链里,而且无需任何信任人。 +
+在平行链的头部都包含一个出口树根(egress-trie-root)。这个树根包含了一个路由信息的列表,列表中的每一项包含有一个串行(concatenated)结构的出口提交。为每条平行链的验证人之间提供默克尔树证明,从而能够证明某条平行链的区块对应着另一条平行链的出口队列。 +通过伪随机数(CSPR)来保证对平行链的公平的配对,防止出现作弊现象。将需要处理的平行链的出口队列与本区块链的入口队列绑定,这样就可以把他们之间的数据进行交易。如果入口队列的数量超出了阈值,那么在中继链上就会被标记为满,在队列重新置空前不再接收新的数据。 +
+波卡链的主要角色如下: +
+ +![role](img/polkadot-role.jpg) +
+ +## 1、收集人 +
+ 收集人用来帮助验证人制造有效的平行区块,他们会运行一个特定平行链的全节点,可以这样认为,收集人是打入平行链内部的中继链的间谍。它可以打包一个区块,将其所得到的所有信息提供给验证人。 +
+ +## 2、验证人 +
+验证人的权限很高,它帮助在波卡链网络里打包新块。不过,这个是有代价的,需要交足够多的押金。当然,有押金的多个提名人也可以推举验证人。但这个时候儿的押金就不是验证人的。而属于提名人。 +
+验证人运行一个中继链的全节点客户端,它通过收集人来得到相关平行链的信息。这里使用一个XBFT的共识算法,保证其履行责任,否则就会扣留其奖励甚至扣减押金。 +
+ +## 3、提名人 +
+类似于矿工,只能把安全性押金委托给验证人,其实没有任何权力。 +
+ +## 4、钓鱼人 +
+钓鱼人类似于赏金猎人,他们用来监控是否有恶意的行为发生。他们只要发现有抵押方想搞事情,就会得到奖励。举一个例子,重复签名。当然,为了保证一定的平衡,奖励从最小开始,逐渐递进的。 +
+当然,做钓鱼人,也是要交少量的押金的,不过比当验证人要少很多。 +
+关于收集人、提名人、验证人的精确关系可能还会修改。这个只是一个目前的状态。 +
+ +# 三、波卡链的源码结构 +
+ +## 1、波卡链的源码结构如下: +
+整体的源码结构: +
+ +![polkadot-source](img/polkadot-source.png) +
+demo:提供了一个客户端简单的例子,它的启动机制类似于波卡链本身的机制,也可以说是某种程度上的复用。 +
+polkadot:波卡链的主文件夹,主要的代码都在这个文件夹内。 +
+safe-mix:混合算法部分。 +
+subkey:只有一个文件,其实就是利用种子发生器生成密钥。 +
+substrate:文件夹中是创建原生平行链的基本模式(包括使用的策略。如共识、存储等)。 +
+ +## 2、polkadot文件夹的代码结构: +
+ +![polkadot-source](img/polkadot-source1.png) +
+api:对外的应用接口,有全节点和轻量节点两种。 +
+cli:客户端 +
+collator:收集者代码 +
+consensus:共识部分代码。 +
+excutor:WASM执行时的本地优化 +
+keystory:密钥存储管理 +
+parachain:平行链编译相关,包手一些数据类型的处理。 +
+primitives:平行链相关的数据基础类型 +
+runtime:平行链的主链的验证者内容及相关WASM的处理。 +
+service:波卡链对网络的控制逻辑,包括通信、线程及网络之间的组织关系。 +
+src:很简单的启动命令客户端。 +
+statement-table:共识相关的存储表 +
+transaction-pool:交易池。 +
+ +## 3、substrate文件夹的代码结构: +
+ +![source2](img/polkadot-source2.png) +
+bft:共识相关。 +
+codec:简单的序列化和反序列化的编码器。 +
+client:客户端。 +
+ed25519:Ed25519椭圆曲线算法接口。 +
+environmental:应用环境的支持,包括堆栈变量的安全引用等。 +
+executor:临时的合约部分和未完成的WASM合约部分。 +
+keyring:代码的运行时支持。 +
+misbehavior-check:错误行为检查。 +
+network:p2p网络相关。 +
+primitives:相关的数据类型、宏定义及属性等。 +
+pwasm-alloc:WASM中相关的分配器的处理。 +
+pwasm-libc:使用标准的LIBC。 +
+rpc:远程调用接口。 +
+rpc-servers:RPC的服务端。 +
+runtime相关:各种不同环境下的运行时支持,从模块化具体到某个功能,到整体上不同的情况,如IO,沙箱等。 +
+serializer:串行化存储,暂时使用JSON,后期可能采用其它。 +
+state-machine:平行链的状态机,包括状态存储等。 +
+ +# 四、总结 +
+波卡链的资料还是比较少的,特别是对源码的支持这方面,基本上没有什么资料。再加上源码由RUST编写,本身这个语言就是一个小众语言。所以从资料到源码的映射就更加复杂。 +
+但是,波卡链的设计架构思想很有借鉴意义,特别是提出中继链打通各个平行链的机制,目前来看仍然很有实用性。 diff --git "a/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\347\275\221\347\273\234.md" "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\347\275\221\347\273\234.md" new file mode 100644 index 0000000..d0f3e7c --- /dev/null +++ "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\270\211\347\275\221\347\273\234.md" @@ -0,0 +1,332 @@ +# 波卡链源码分析之三网络 + +在启动的流程里其实是挖了一些坑的,至少网络的坑在这次分析中尽量填上,在网络分析中重点分析一下网络发现,由于对RUST不是多熟悉,并且波卡链中使用了好多的第三方库,所以界定起来可能有点麻烦。 +## 一、启动网络节点 +
+接着上篇分析,从network.start_network()开始,它会调用selt.start()(substrate/network/src/service.rs): +
+ +``` rust +fn start(&self) { + match self.network.start().map_err(Into::into) { + Err(ErrorKind::Io(ref e)) if e.kind() == io::ErrorKind::AddrInUse => + warn!("Network port {:?} is already in use, make sure that another instance of Polkadot client is not running or change the port using the --port option.", self.network.config().listen_address.expect("Listen address is not set.")), + Err(err) => warn!("Error starting network: {}", err), + _ => {}, + }; + self.network.register_protocol(self.handler.clone(), DOT_PROTOCOL_ID, &[(0, V0_PACKET_COUNT)]) + .unwrap_or_else(|e| warn!("Error registering polkadot protocol: {:?}", e)); +} +``` +
+这个函数就两个功能:启动网络并注册通信协议。同时处理这两个功能实现时出现的异常。 +
+下面看一下连接上来的节点的控制: +
+ +``` rust +/// On-demand service API. +pub trait OnDemandService: Send + Sync { + /// When new node is connected. + fn on_connect(&self, peer: PeerId, role: service::Role); + + /// When node is disconnected. + fn on_disconnect(&self, peer: PeerId); + + /// Maintain peers requests. + fn maintain_peers(&self, io: &mut SyncIo); + + /// When response is received from remote node. + fn on_remote_response(&self, io: &mut SyncIo, peer: PeerId, response: message::RemoteCallResponse); +} + +/// On-demand requests service. Dispatches requests to appropriate peers. +pub struct OnDemand { + core: Mutex>, + checker: Arc, +} +//赋值给默认值使用的属性 +#[derive(Default)] +struct OnDemandCore { + service: Weak, + next_request_id: u64, + pending_requests: VecDeque, + active_peers: LinkedHashMap, + idle_peers: VecDeque, +} +``` +
+上面的三个数据结构是管理服务中的基础数据结构,在整个服务中都用得到。看一下如何控制连接节点。 +
+ +``` rust +fn on_connect(&self, peer: PeerId, role: service::Role) { + if !role.intersects(service::Role::FULL | service::Role::COLLATOR | service::Role::VALIDATOR) { // TODO: correct? + return; + } + + let mut core = self.core.lock(); + core.add_peer(peer); + core.dispatch(); +} + +pub fn add_peer(&mut self, peer: PeerId) { + self.idle_peers.push_back(peer); +} +``` +
+先创建空闲节点,然后再得到连接数据后将其与实际的远端节点绑定: +
+ +``` rust +pub fn dispatch(&mut self) { + let service = match self.service.upgrade() { + Some(service) => service, + None => return, + }; + + while !self.pending_requests.is_empty() { + let peer = match self.idle_peers.pop_front() { + Some(peer) => peer, + None => return, + }; + + let mut request = self.pending_requests.pop_front().expect("checked in loop condition; qed"); + request.timestamp = Instant::now(); + trace!(target: "sync", "Dispatching remote request {} to peer {}", request.id, peer); + + service.execute_in_context(|ctx, protocol| { + let message = message::RemoteCallRequest { + id: request.id, + block: request.request.block, + method: request.request.method.clone(), + data: request.request.call_data.clone(), + }; + + protocol.send_message(ctx, peer, message::Message::RemoteCallRequest(message)) + }); + self.active_peers.insert(peer, request); + } +} +//发送请求的块内容,如果出错,断开PEER。 +pub fn send_message(&self, io: &mut SyncIo, peer_id: PeerId, mut message: Message) { + match &mut message { + &mut Message::BlockRequest(ref mut r) => { + let mut peers = self.peers.write(); + if let Some(ref mut peer) = peers.get_mut(&peer_id) { + r.id = peer.next_request_id; + peer.next_request_id = peer.next_request_id + 1; + peer.block_request = Some(r.clone()); + peer.request_timestamp = Some(time::Instant::now()); + } + }, + _ => (), + } + let data = serde_json::to_vec(&message).expect("Serializer is infallible; qed"); + if let Err(e) = io.send(peer_id, data) { + debug!(target:"sync", "Error sending message: {:?}", e); + io.disconnect_peer(peer_id); + } +} +``` +
+ +## 二、网络的消息分发 +
+ +类似于所有的区块链都要有一个分发的处理函数(network/src/protocol.rs),由read函数调用其它(network/src/service.rs=>fn read())来实现: +
+ +``` rust +pub fn handle_packet(&self, io: &mut SyncIo, peer_id: PeerId, data: &[u8]) { + let message: Message = match serde_json::from_slice(data) { + Ok(m) => m, + Err(e) => { + debug!("Invalid packet from {}: {}", peer_id, e); + io.disable_peer(peer_id); + return; + } + }; + +//又看到了匹配 + match message { + Message::Status(s) => self.on_status_message(io, peer_id, s), + Message::BlockRequest(r) => self.on_block_request(io, peer_id, r), + Message::BlockResponse(r) => { + let request = { + let mut peers = self.peers.write(); + if let Some(ref mut peer) = peers.get_mut(&peer_id) { + peer.request_timestamp = None; + match mem::replace(&mut peer.block_request, None) { + Some(r) => r, + None => { + debug!("Unexpected response packet from {}", peer_id); + io.disable_peer(peer_id); + return; + } + } + } else { + debug!("Unexpected packet from {}", peer_id); + io.disable_peer(peer_id); + return; + } + }; + if request.id != r.id { + trace!(target: "sync", "Ignoring mismatched response packet from {} (expected {} got {})", peer_id, request.id, r.id); + return; + } + self.on_block_response(io, peer_id, request, r); + }, + Message::BlockAnnounce(announce) => { + self.on_block_announce(io, peer_id, announce); + }, + Message::Statement(s) => self.on_statement(io, peer_id, s, blake2_256(data).into()), + Message::CandidateRequest(r) => self.on_candidate_request(io, peer_id, r), + Message::CandidateResponse(r) => self.on_candidate_response(io, peer_id, r), + Message::BftMessage(m) => self.on_bft_message(io, peer_id, m, blake2_256(data).into()), + Message::Transactions(m) => self.on_transactions(io, peer_id, m), + Message::RemoteCallRequest(request) => self.on_remote_call_request(io, peer_id, request), + Message::RemoteCallResponse(response) => self.on_remote_call_response(io, peer_id, response) + } +} +``` +
+首先得到message,判断其是否正确。下来就是对消息进行分类处理,如果状态,声明,区块请求,侯选请求等.这个其实就类似于其它语言中的switch-case语句。在每个本地的self的函数中,调用相关的变量匹配函数,这个其实就是RUST中的use的用法,将其它名字空间的函数引用到本地来,这里举一个例子:self.on_candidate_request(io, peer_id, r) +
+ +``` rust +fn on_candidate_request(&self, io: &mut SyncIo, peer: PeerId, request: message::CandidateRequest) { + trace!(target: "sync", "CandidateRequest {} from {} for {}", request.id, peer, request.hash); + self.consensus.lock().on_candidate_request(io, self, peer, request); +} +//network/src/consensus.rs +pub fn on_candidate_request(&mut self, io: &mut SyncIo, protocol: &Protocol, peer_id: PeerId, request: message::CandidateRequest) { + let response = match self.our_candidate { + Some((ref hash, ref data)) if *hash == request.hash => Some(data.clone()), + _ => None, + }; + let msg = message::CandidateResponse { + id: request.id, + data: response, + }; + protocol.send_message(io, peer_id, Message::CandidateResponse(msg)); +} +``` +
+在共识算法中,侯选人的请求函数,因为在波卡链中使用是一个基于BFT的算法,所以它仍然会有一个选举的过程。 +
+ +## 三、平行链 +
+ +在一个collator中,会创建一个平行链的全节点: +
+ +``` rust +Box::new(stream::futures_unordered(egress_fetch) + .fold(BTreeMap::new(), |mut map, (routing_id, egresses)| { + for (depth, egress) in egresses.into_iter().rev().enumerate() { + let depth = -(depth as i64); + map.insert((depth, routing_id), egress); + } + + Ok(map) + }) + .map(|ordered| ordered.into_iter().map(|((_, id), egress)| (id, egress))) + .map(|i| i.collect::>()) + .map(ConsolidatedIngress)) +} + +/// Produce a candidate for the parachain. +pub fn collate<'a, R, P>(local_id: ParaId, relay_context: R, para_context: P) +-> Box + 'a> +where + R: RelayChainContext, + R::Error: 'a, + R::FutureEgress: 'a, + P: ParachainContext + 'a, +{ +Box::new(collate_ingress(relay_context).map(move |ingress| { + let (block_data, _, signature) = para_context.produce_candidate( + ingress.0.iter().flat_map(|&(id, ref msgs)| msgs.iter().cloned().map(move |msg| (id, msg))) + ); + + parachain::Candidate { + parachain_index: local_id, + collator_signature: signature, + block: block_data, + unprocessed_ingress: ingress, + } +})) +} +``` +
+在map中会注册相关的路由信息。这里其实是通向链路由的一个必经之路。然后在共识的服务中会有一个路由的控制(consensus/src/service.rs),目的是可以顺利的找到共识的节点: +
+ +``` rust +type FetchCandidateAdapter = future::Map) -> BlockData>; + +#[derive(Clone)] +struct Router { + network: Arc, +} + +impl Router { + fn fetch_candidate_adapter(data: Vec) -> BlockData { + BlockData(data) + } +} + +impl TableRouter for Router { + type Error = Canceled; + type FetchCandidate = FetchCandidateAdapter; + type FetchExtrinsic = future::FutureResult; + + fn local_candidate_data(&self, hash: Hash, block_data: BlockData, _extrinsic: Extrinsic) { + let data = block_data.0; + self.network.set_local_candidate(Some((hash, data))) + } + + fn fetch_block_data(&self, candidate: &CandidateReceipt) -> Self::FetchCandidate { + let hash = candidate.hash(); + self.network.fetch_candidate(&hash).map(Self::fetch_candidate_adapter) + } + + fn fetch_extrinsic_data(&self, _candidate: &CandidateReceipt) -> Self::FetchExtrinsic { + future::ok(Extrinsic) + } +} +``` +
+它的意义在于批准不同的平行链的区块后就可以进行中继链的区块的批准,然后正式提交区块。另外在协议中也会有相关的配置: +
+ +``` rust +/// Called when a new peer is connected +pub fn on_peer_connected(&self, io: &mut SyncIo, peer_id: PeerId) { + trace!(target: "sync", "Connected {}: {}", peer_id, io.peer_info(peer_id)); + self.handshaking_peers.write().insert(peer_id, time::Instant::now()); + self.send_status(io, peer_id); +} +/// Send Status message +fn send_status(&self, io: &mut SyncIo, peer_id: PeerId) { + if let Ok(info) = self.chain.info() { + let status = message::Status { + version: PROTOCOL_VERSION, + genesis_hash: info.chain.genesis_hash, + roles: self.config.roles.into(), + best_number: info.chain.best_number, + best_hash: info.chain.best_hash, + validator_signature: None, + validator_id: None, + parachain_id: None, + }; + self.send_message(io, peer_id, Message::Status(status)) + } +} +``` +
+这就和前面的服务启动对应上了,在启动时,会自动注册相关的节点状态。然后配以路由,进行转发。 +
+
diff --git "a/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\345\220\257\345\212\250\346\265\201\347\250\213.md" "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\345\220\257\345\212\250\346\265\201\347\250\213.md" new file mode 100644 index 0000000..2733000 --- /dev/null +++ "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\214\345\220\257\345\212\250\346\265\201\347\250\213.md" @@ -0,0 +1,508 @@ + +# 波卡链源码分析之二启动流程 + +## 一、启动命令端 +
+RUST语言使用cargo创建工程有一个好的地方,库是lib.rs标记,主程序main.rs标记,所以相对别的语言明显好找启动点。波卡链的主程序在polkadot/polkadot/src/main.rs中,打开这个文件: +
+ +``` rust +#![warn(missing_docs)] //忽略丢失文档 + +extern crate polkadot_cli as cli; + +#[macro_use] +extern crate error_chain; + +quick_main!(run); + +fn run() -> cli::error::Result<()> { + cli::run(::std::env::args()) +} +``` +
+代码很简单,不过风格似乎在哪里见过?是不是和以太坊等的启动代码有些类似,只是这个更单薄一些。可能大家对RUST不太了解,所以在分析波卡链时同时对RUST一些知识进行说明。 +
+quick_main!(run);这个是一个宏,能够产生main主函数,它在error_chain这个库中,所以上面要导入这个库。它除了能够产生主函数,还可以调用其下的函数并返回相关的错误结果Result<()>,这里当然就是run这个函数了。#[macro_use]是属性设置,加上!是代表包含的自身属性设置,不加代表下一个项目的属性设置。这里是说明可以把宏应用到当前的作用域中。 +
+所以,可以进入run函数(cli/src/lib.rs): +
+ +``` rust +pub fn run(args: I) -> error::Result<()> where + I: IntoIterator, + T: Into + Clone, +{ + //这个是主角,一个异步IO的栈,网络库 + let core = reactor::Core::new().expect("tokio::Core could not be created"); + + let yaml = load_yaml!("./cli.yml");//polkadot启动的配置参数在yml文件中,加载之 + //RUST的SWITCH + let matches = match clap::App::from_yaml(yaml).version(crate_version!()).get_matches_from_safe(args) { + Ok(m) => m, + Err(ref e) if e.kind == clap::ErrorKind::VersionDisplayed => return Ok(()), + Err(ref e) if e.kind == clap::ErrorKind::HelpDisplayed || e.kind == clap::ErrorKind::VersionDisplayed => { + let _ = clap::App::from_yaml(yaml).print_long_help();//类似default + return Ok(());//如果是非链环境(帮助之类),则返回。 + } + Err(e) => return Err(e.into()), + }; + + // TODO [ToDr] Split parameters parsing from actual execution. + let log_pattern = matches.value_of("log").unwrap_or(""); + init_logger(log_pattern); + fdlimit::raise_fd_limit(); + + let mut config = service::Configuration::default(); + + let base_path = matches.value_of("base-path") + .map(|x| Path::new(x).to_owned()) + .unwrap_or_else(default_base_path); + + //keystroe的参数处理 + config.keystore_path = matches.value_of("keystore") + .map(|x| Path::new(x).to_owned()) + .unwrap_or_else(|| keystore_path(&base_path)) + .to_string_lossy() + .into(); + + config.database_path = db_path(&base_path).to_string_lossy().into(); + + //角色处理,四种角色,其使用的是network中的Role:pub use network::Role; + let mut role = service::Role::FULL; + if matches.is_present("collator") { + info!("Starting collator."); + role = service::Role::COLLATOR; + } else if matches.is_present("validator") { + info!("Starting validator."); + role = service::Role::VALIDATOR; + } else if matches.is_present("light") { + info!("Starting light."); + role = service::Role::LIGHT; + } + + match matches.value_of("chain") { + Some("dev") => config.chain_spec = ChainSpec::Development, + Some("local") => config.chain_spec = ChainSpec::LocalTestnet, + Some("poc-1") => config.chain_spec = ChainSpec::PoC1Testnet, + None => (), + Some(unknown) => panic!("Invalid chain name: {}", unknown), + } + info!("Chain specification: {}", match config.chain_spec { + ChainSpec::Development => "Development", + ChainSpec::LocalTestnet => "Local Testnet", + ChainSpec::PoC1Testnet => "PoC-1 Testnet", + }); + + config.roles = role; + { + //配置启动节点和相关网络参数 + config.network.boot_nodes = matches + .values_of("bootnodes") + .map_or(Default::default(), |v| v.map(|n| n.to_owned()).collect()); + config.network.config_path = Some(network_path(&base_path).to_string_lossy().into()); + config.network.net_config_path = config.network.config_path.clone(); + + let port = match matches.value_of("port") { + Some(port) => port.parse().expect("Invalid p2p port value specified."), + None => 30333, + }; + config.network.listen_address = Some(SocketAddr::new("0.0.0.0".parse().unwrap(), port)); + config.network.public_address = None; + config.network.client_version = format!("parity-polkadot/{}", crate_version!()); + config.network.use_secret = match matches.value_of("node-key").map(|s| s.parse()) { + Some(Ok(secret)) => Some(secret), + Some(Err(err)) => return Err(format!("Error parsing node key: {}", err).into()), + None => None, + }; + } + + config.keys = matches.values_of("key").unwrap_or_default().map(str::to_owned).collect(); + + //根据角色来创建全节点或者轻量级节点 + match role == service::Role::LIGHT { + true => run_until_exit(core, service::new_light(config.clone())?, &matches, config), + false => run_until_exit(core, service::new_full(config.clone())?, &matches, config), + } +} +``` +
+这个函数其实没啥,类似于所有的区块链启动,对参数进行解析,并启动相关的操作,一步步分析别急。其实最关键提最后两行,也就是注释中说明的轻节点和全节点。 +
+先看一下这个函数: +
+ +``` rust +//rust的模板模板编程,WHERE类似于C#的限定,最后一个参数是一个函数指针 +fn run_until_exit(mut core: reactor::Core, service: service::Service, matches: &clap::ArgMatches, config: service::Configuration) -> error::Result<()> + where + B: client::backend::Backend + Send + Sync + 'static,//静态的生命周期 + E: client::CallExecutor + Send + Sync + 'static, + client::error::Error: From<<::State as state_machine::backend::Backend>::Error> +{ + let exit = { + // can't use signal directly here because CtrlC takes only `Fn`.退出控制 + let (exit_send, exit) = mpsc::channel(1); + //移动语义 + ctrlc::CtrlC::set_handler(move || { + exit_send.clone().send(()).wait().expect("Error sending exit notification"); + }); + + //推导返回值,可以使用return,但不推荐 + exit + }; + + //启动钓鱼服务 + informant::start(&service, core.handle()); + + //创建RPC和相关的处理句柄 + let _rpc_servers = { + let http_address = parse_address("127.0.0.1:9933", "rpc-port", matches)?;//问号表示可以有0~1个匹配 + let ws_address = parse_address("127.0.0.1:9944", "ws-port", matches)?; + + //||是闭包,其后为闭包表达式 + let handler = || { + let chain = rpc::apis::chain::Chain::new(service.client(), core.remote()); + let pool = RpcTransactionPool { + inner: service.transaction_pool(), + network: service.network(), + }; + //创建rpc_handler,代码分析见后面的RPC + rpc::rpc_handler(service.client(), chain, pool, Configuration(config.clone())) + }; + //处理元组,自动推导 + ( + //|address|是一个管道,用在闭包中。启动两个服务 + start_server(http_address, |address| rpc::start_http(address, handler())), + start_server(ws_address, |address| rpc::start_ws(address, handler())), + ) + }; + + //运行异步IO,启动网络通信 + core.run(exit.into_future()).expect("Error running informant event loop"); + Ok(()) +} +``` +
+再看一下启动的服务: +
+ +``` rust +fn start_server(mut address: SocketAddr, start: F) -> Result where + F: Fn(&SocketAddr) -> Result, +{ + start(&address) + .or_else(|e| match e.kind() { + io::ErrorKind::AddrInUse | + io::ErrorKind::PermissionDenied => { + warn!("Unable to bind server to {}. Trying random port.", address); + address.set_port(0); + start(&address) + }, + _ => Err(e),//一定要有默认分支 + }) +} +``` +
+一个泛型函数,调用F函数指针,也就是前面的rpc::start_http和rpc::start_ws两个函数,编程语言越高级,越脱离人类的形式化感知,不知道是好事还是坏事。or_else是Result的一个结果分支语句。 +
+基本上到这儿,相关的命令就全启动了。然后分析一下具体的细节。 + + +## 二、相关服务 +
+下面分别来看钓鱼服务和RPC服务: +
+ +### 1、钓鱼服务 +
+polkadot/cli/src/informant.rs +
+ +``` rust + +/// Spawn informant on the event loop +pub fn start(service: &Service, handle: reactor::Handle) + where + B: client::backend::Backend + Send + Sync + 'static,//where限定子句,+号就是多重限制(继承) + E: client::CallExecutor + Send + Sync + 'static, + client::error::Error: From<<::State as state_machine::backend::Backend>::Error> +{ + let interval = reactor::Interval::new_at(Instant::now(), Duration::from_millis(TIMER_INTERVAL_MS), &handle) + .expect("Error creating informant timer"); + + //调用client(&self)等得到Clone值 + let network = service.network(); + let client = service.client(); + + let display_notifications = interval.map_err(|e| debug!("Timer error: {:?}", e)).for_each(move |_| { + let sync_status = network.status(); + + //if let语义 + if let Ok(best_block) = client.best_block_header() { + let hash: HeaderHash = best_block.blake2_256().into(); + let status = match (sync_status.sync.state, sync_status.sync.best_seen_block) { + (SyncState::Idle, _) => "Idle".into(), + (SyncState::Downloading, None) => "Syncing".into(), + (SyncState::Downloading, Some(n)) => format!("Syncing, target=#{}", n), + }; + info!(target: "polkadot", "{} ({} peers), best: #{} ({})", status, sync_status.num_peers, best_block.number, hash) + } else { + warn!("Error getting best block information"); + } + Ok(()) + }); + + let client = service.client();//因为闭包,所以这里需要再次获得 + let display_block_import = client.import_notification_stream().for_each(|n| { + info!(target: "polkadot", "Imported #{} ({})", n.header.number, n.hash); + Ok(()) + }); + + //处理通知和数据块的导入 + handle.spawn(display_notifications); + handle.spawn(display_block_import); +} +``` +
+使用 handle.spawn 将函数 Future 绑定到 event loop 上面。只有如此在处理并发连接时不会阻塞 event loop。写过异步I/O 的都会明白 socket 是非阻塞的即不可能通过一次 read 或者 write 就将这个 socket 的数据全部处理完成。因此还会重新将 socket 给注册到 event loop,这样当有新的事件的时候,event loop 会重新调用对应的回调函数。 +tokio_core提供了这么一个异步IO的机制,确实比较好用。 +
+ +### 2、RPC服务 +
+在上面的创建服务时,没有分析创建两类节点: +
+polkadot/service/src/lib.rs +
+ +``` rust +/// Creates light client and register protocol with the network service轻节点 +pub fn new_light(config: Configuration) -> Result>>, error::Error> { + //下划杠代表忽略绑定,类似于GO,MOVE语义加上管道形成一个闭包 + Service::new(move |_, executor, genesis_builder: GenesisBuilder| { + let client_backend = client::light::new_light_backend(); + let fetch_checker = Arc::new(client::light::new_fetch_checker(client_backend.clone(), executor)); + let fetcher = Arc::new(network::OnDemand::new(fetch_checker)); + let client = client::light::new_light(client_backend, fetcher.clone(), genesis_builder)?; + Ok((Arc::new(client), Some(fetcher))) + }, + |client| Arc::new(polkadot_api::light::RemotePolkadotApiWrapper(client.clone())), + |_client, _network, _tx_pool, _keystore| Ok(None), + config) +} + +/// Creates full client and register protocol with the network service全节点多了数据库的相关数据操作 +pub fn new_full(config: Configuration) -> Result>, error::Error> { + let is_validator = (config.roles & Role::VALIDATOR) == Role::VALIDATOR; + Service::new(|db_settings, executor, genesis_builder: GenesisBuilder| + Ok((Arc::new(client_db::new_client(db_settings, executor, genesis_builder)?), None)), + |client| client, + |client, network, tx_pool, keystore| { + if !is_validator { + return Ok(None); + } + + // Load the first available key. Code above makes sure it exisis. + let key = keystore.load(&keystore.contents()?[0], "")?; + info!("Using authority key {:?}", key.public()); + //见下面推荐牟NEW + Ok(Some(consensus::Service::new( + client.clone(), + client.clone(), + network.clone(), + tx_pool.clone(), + ::std::time::Duration::from_millis(4000), // TODO: dynamic + key, + ))) + }, + config) +} +/// Creates and register protocol with the network service这里很重要,是真正创建网络服务的地方 +fn new(client_creator: F, api_creator: G, consensus_creator: C, mut config: Configuration) -> Result + where //RUST的WHERE这么做确实有点丑陋啊 + F: FnOnce( + client_db::DatabaseSettings, + CodeExecutor, + GenesisBuilder, + ) -> Result<(Arc>, Option>>), error::Error>, + G: Fn( + Arc>, + ) -> Arc, + C: Fn( + Arc>, + Arc, + Arc>, + &Keystore + ) -> Result, error::Error>, + A: PolkadotApi + Send + Sync + 'static, +{ + use std::sync::Barrier;//同步的内存屏障 + + let (signal, exit) = ::exit_future::signal();//事件元组 + + // Create client + let executor = polkadot_executor::Executor::new(); + + let mut keystore = Keystore::open(config.keystore_path.into())?; + for seed in &config.keys { + keystore.generate_from_seed(seed)?;//产生随机种子 + } + + //由上边的种子生产密钥对 + if keystore.contents()?.is_empty() { + let key = keystore.generate("")?; + info!("Generated a new keypair: {:?}", key.public()); + } + + let ChainConfig { genesis_config, boot_nodes } = match config.chain_spec { + ChainSpec::Development => development_config(), + ChainSpec::LocalTestnet => local_testnet_config(), + ChainSpec::PoC1Testnet => poc_1_testnet_config(), + }; + config.network.boot_nodes.extend(boot_nodes); + + let genesis_builder = GenesisBuilder { + config: genesis_config, + }; + + let db_settings = client_db::DatabaseSettings { + cache_size: None, + path: config.database_path.into(), + }; + + //创建客户端 + let (client, on_demand) = client_creator(db_settings, executor, genesis_builder)?; + let api = api_creator(client.clone()); + let best_header = client.best_block_header()?; + info!("Starting Polkadot. Best block is #{}", best_header.number); + //创建交易池及其适配器 + let transaction_pool = Arc::new(Mutex::new(TransactionPool::new(config.transaction_pool))); + let transaction_pool_adapter = Arc::new(TransactionPoolAdapter { + pool: transaction_pool.clone(), + client: client.clone(), + api: api.clone(), + }); + let network_params = network::Params { + config: network::ProtocolConfig { + roles: config.roles, + }, + network_config: config.network, + chain: client.clone(), + on_demand: on_demand.clone().map(|d| d as Arc), + transaction_pool: transaction_pool_adapter, + }; + + //创建网络通信服务 + let network = network::Service::new(network_params)?; + let barrier = ::std::sync::Arc::new(Barrier::new(2)); + //引入多线程原子变量控制 + on_demand.map(|on_demand| on_demand.set_service_link(Arc::downgrade(&network))); + + let thread = { + let client = client.clone(); + let network = network.clone(); + let txpool = transaction_pool.clone(); + + let thread_barrier = barrier.clone(); + 启动多线程并挂载启动网络等事件函数 + thread::spawn(move || { + network.start_network(); + + thread_barrier.wait(); + let mut core = Core::new().expect("tokio::Core could not be created"); + let events = client.import_notification_stream().for_each(move |notification| { + network.on_block_imported(notification.hash, ¬ification.header);//导入区块 + prune_imported(&*client, &*txpool, notification.hash);//处理导入的区块 + + Ok(()) + }); + + core.handle().spawn(events);//绑定事件 + if let Err(e) = core.run(exit) { + debug!("Polkadot service event loop shutdown with {:?}", e); + } + debug!("Polkadot service shutdown"); + }) + }; + + // wait for the network to start up before starting the consensus + // service. + barrier.wait(); + + // Spin consensus service if configured创建共识创建者 + let consensus_service = consensus_creator(client.clone(), network.clone(), transaction_pool.clone(), &keystore)?; + + Ok(Service { + thread: Some(thread), + client: client, + network: network, + transaction_pool: transaction_pool, + signal: Some(signal), + _consensus: consensus_service, + }) +} +``` + +
+这个还是有一些类似于以太坊,创建节点时把相关的网络服务都创建好。这样最后Ok返回让RUST自动推导结果。 +
+把前面的服务分析明白后,就开始调用真正的RPC中的服务的分析: +
+substate/rpc-servers/src/lib.rs +
+ +``` rust +/// Construct rpc `IoHandler` +pub fn rpc_handler( + state: S, + chain: C, + author: A, + system: Y, +) -> RpcHandler where + S: apis::state::StateApi, + C: apis::chain::ChainApi, + A: apis::author::AuthorApi, + Y: apis::system::SystemApi, +{ + let mut io = pubsub::PubSubHandler::default(); + io.extend_with(state.to_delegate()); + io.extend_with(chain.to_delegate()); + io.extend_with(author.to_delegate()); + io.extend_with(system.to_delegate()); + io +} + +/// Start HTTP server listening on given address. +pub fn start_http( + addr: &std::net::SocketAddr, + io: RpcHandler, +) -> io::Result { + http::ServerBuilder::new(io) + .threads(4) + .rest_api(http::RestApi::Unsecure) + .cors(http::DomainsValidation::Disabled) + .start_http(addr) +} + +/// Start WS server listening on given address. +pub fn start_ws( + addr: &std::net::SocketAddr, + io: RpcHandler, +) -> io::Result { + ws::ServerBuilder::with_meta_extractor(io, |context: &ws::RequestContext| Metadata::new(context.sender())) + .start(addr) + .map_err(|err| match err { + ws::Error(ws::ErrorKind::Io(io), _) => io, + ws::Error(ws::ErrorKind::ConnectionClosed, _) => io::ErrorKind::BrokenPipe.into(), + ws::Error(e, _) => { + error!("{}", e); + io::ErrorKind::Other.into() + } + }) +} +``` +
+这两个函数里都使用了JSON的库,直接就构建了相关的服务,这两个库的资料目前找不到,所以没办法深入的分析说明。这里分析完成了相关的服务启动,下一次重点分析网络相关具体内容。 +
+
diff --git "a/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\214\272\345\235\227\345\217\212\345\205\261\350\257\206.md" "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\214\272\345\235\227\345\217\212\345\205\261\350\257\206.md" new file mode 100644 index 0000000..f63b2e1 --- /dev/null +++ "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\344\272\224\345\214\272\345\235\227\345\217\212\345\205\261\350\257\206.md" @@ -0,0 +1,913 @@ +# 波卡链源码分析之五区块及共识 + +## 一、区块和区块头数据结构 + +
+看一下区块的结构:(substate/primitives/src/block.rs) +
+ +``` rust +/// Simple generic extrinsic type. +//核心就是一个BYTE数组 +#[derive(PartialEq, Eq, Clone)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +pub struct Extrinsic(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec); + +//定义块结构并设置相关属性 +#[derive(PartialEq, Eq, Clone)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +pub struct Block { + /// The block header. + pub header: Header, + /// All relay-chain transactions. + pub transactions: Vec, +} +//对Block的编码解码 +impl Slicable for Block where Vec: Slicable { + fn decode(input: &mut I) -> Option { + Some(Block { + header: Slicable::decode(input)?, + transactions: Slicable::decode(input)?, + }) + } + fn encode(&self) -> Vec { + let mut v: Vec = Vec::new(); + v.extend(self.header.encode()); + v.extend(self.transactions.encode()); + v + } +} + ///正式定义Block. + pub type Block = generic::Block; +``` +
+从数据结构上来看,Block包含Header和Transaction的一个数组向量。和其它的链没有太大的区别。重点看一下头: +
+ +``` rust +/// A substrate chain block header. +// TODO: split out into light-client-specific fields and runtime-specific fields. +#[derive(PartialEq, Eq, Clone)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "std", serde(deny_unknown_fields))] +pub struct Header { + /// Block parent's hash. + pub parent_hash: HeaderHash,//父哈希,成链的前提 + /// Block number. + pub number: Number,//块号 + /// State root after this transition. + pub state_root: Hash,//状态哈希的根 + /// The root of the trie that represents this block's transactions, indexed by a 32-byte integer. + pub extrinsics_root: Hash,//trie的根哈希,类似于以太坊的三大哈希树 + // TODO... +// /// The root of the trie that represents the receipts from this block's transactions +// pub receipts_root: Hash,//收据哈希根 + /// The digest of activity on the block. + pub digest: Digest,//摘要 +} +``` +
+中继链的区块其实和其它的链的块结构原理基本都是类似的,没有什么特别的地方,可能不同的就是要有一个外部默克尔树的提交的HASH,但也可以透明的理解成相关的数据哈希。 +
+ +## 二、区块的生产 + +
+波卡链的区块产生分为以下几步: +
+1、由收集者产生区块 +
+在波卡链中,对上面的块进行了进一步的抽象: +
+ +``` rust +/// Abstraction over a substrate block. +#[derive(PartialEq, Eq, Clone)] +#[cfg_attr(feature = "std", derive(Debug, Serialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "std", serde(deny_unknown_fields))] +pub struct Block where + Number: Member, + Hash: Member, + DigestItem: Member, + AccountId: Member, + Index: Member, + Call: Member, + Signature: Member +{ + /// The block header. + pub header: Header, + /// The accompanying extrinsics. + pub extrinsics: Vec>, +} + +impl Slicable + for Block +where + Number: Member, + Hash: Member, + DigestItem: Member, + AccountId: Member, + Index: Member, + Call: Member, + Signature: Member, + Header: Slicable, + UncheckedExtrinsic: Slicable, +{ + fn decode(input: &mut I) -> Option { + Some(Block { + header: Slicable::decode(input)?, + extrinsics: Slicable::decode(input)?, + }) + } + fn encode(&self) -> Vec { + let mut v: Vec = Vec::new(); + v.extend(self.header.encode()); + v.extend(self.extrinsics.encode()); + v + } +} + +impl traits::Block + for Block +where + Number: Member + Slicable, + Hash: Member + Slicable, + DigestItem: Member + Slicable, + AccountId: Member, + Index: Member, + Call: Member, + Signature: Member +{ + type Extrinsic = UncheckedExtrinsic; + type Header = Header; + fn header(&self) -> &Self::Header { + &self.header + } + fn extrinsics(&self) -> &[Self::Extrinsic] { + &self.extrinsics[..] + } + fn deconstruct(self) -> (Self::Header, Vec) { + (self.header, self.extrinsics) + } +} +//此处为交易池代码: +/// Useful functions for working with Polkadot blocks. +pub struct PolkadotBlock { + block: Block, + location: Option<(&'static str, usize)>, +} + +impl PolkadotBlock { + /// Create a new block, checking high-level well-formedness. + pub fn from(unchecked: Block) -> ::std::result::Result { + if unchecked.extrinsics.len() < 2 { + return Err(unchecked); + } + if unchecked.extrinsics[0].is_signed() { + return Err(unchecked); + } + if unchecked.extrinsics[1].is_signed() { + return Err(unchecked); + } + + match unchecked.extrinsics[0].extrinsic.function { + Call::Timestamp(TimestampCall::set(_)) => {}, + _ => return Err(unchecked), + } + + match unchecked.extrinsics[1].extrinsic.function { + Call::Parachains(ParachainsCall::set_heads(_)) => {}, + _ => return Err(unchecked), + } + + // any further checks... + Ok(PolkadotBlock { block: unchecked, location: None }) + } + + /// Create a new block, skipping any high-level well-formedness checks. WARNING: This could + /// result in internal functions panicking if the block is, in fact, not well-formed. + pub fn force_from(known_good: Block, file: &'static str, line: usize) -> Self { + PolkadotBlock { block: known_good, location: Some((file, line)) } + } + + /// Retrieve the timestamp of a Polkadot block. + pub fn timestamp(&self) -> Timestamp { + if let Call::Timestamp(TimestampCall::set(t)) = self.block.extrinsics[0].extrinsic.function { + t + } else { + if let Some((file, line)) = self.location { + panic!("Invalid block used in `PolkadotBlock::force_from` at {}:{}", file, line); + } else { + panic!("Invalid block made it through the PolkadotBlock verification!?"); + } + } + } + + /// Retrieve the parachain candidates proposed for this block. + pub fn parachain_heads(&self) -> &[CandidateReceipt] { + if let Call::Parachains(ParachainsCall::set_heads(ref t)) = self.block.extrinsics[1].extrinsic.function { + &t[..] + } else { + if let Some((file, line)) = self.location { + panic!("Invalid block used in `PolkadotBlock::force_from` at {}:{}", file, line); + } else { + panic!("Invalid block made it through the PolkadotBlock verification!?"); + } + } + } +} + +#[macro_export] +macro_rules! assert_polkadot_block { + ($known_good:expr) => ( PolkadotBlock::force_from(known_good, file!(), line!()) ) +} + +impl ::std::ops::Deref for PolkadotBlock { + type Target = Block; + fn deref(&self) -> &Block { + &self.block + } +} + +impl From for Block { + fn from(pd: PolkadotBlock) -> Self { + pd.block + } +} + +/// Iterator over pending transactions. +pub type PendingIterator<'a, C> = + transaction_pool::PendingIterator<'a, VerifiedTransaction, Ready<'a, C>, Scoring, NoopListener>; + +error_chain! { + errors { + /// Attempted to queue an inherent transaction. + IsInherent(xt: UncheckedExtrinsic) { + description("Inherent transactions cannot be queued."), + display("Inehrent transactions cannot be queued."), + } + /// Attempted to queue a transaction with bad signature. + BadSignature(xt: UncheckedExtrinsic) { + description("Transaction had bad signature."), + display("Transaction had bad signature."), + } + /// Attempted to queue a transaction that is already in the pool. + AlreadyImported(hash: Hash) { + description("Transaction is already in the pool."), + display("Transaction {:?} is already in the pool.", hash), + } + /// Import error. + Import(err: Box<::std::error::Error + Send>) { + description("Error importing transaction"), + display("Error importing transaction: {}", err.description()), + } + } +} +``` +
+ +## 三、共识 + +
+共识主要在consensus/src中,在substrate/primitives/bft.rs中是基本的BFT共识算法。 +
+1、共识的发起 +
+ +``` rust +/// The Polkadot proposer logic. +pub struct Proposer { + client: Arc, + collators: P, + delay: Shared, + dynamic_inclusion: DynamicInclusion, + handle: Handle, + local_duty: LocalDuty, + local_key: Arc, + parent_hash: HeaderHash, + parent_id: C::CheckedBlockId, + parent_number: BlockNumber, + random_seed: Hash, + router: R, + table: Arc, + transaction_pool: Arc>, +} +//启动共识 +fn start_bft( + header: &Header, + handle: reactor::Handle, + client: &bft::Authorities, + network: Arc, + bft_service: &BftService, +) where + F: bft::ProposerFactory + 'static, + C: bft::BlockImport + bft::Authorities + 'static, + ::Error: ::std::fmt::Debug, + ::Error: ::std::fmt::Display + Into, +{ + let parent_hash = header.blake2_256().into(); + if bft_service.live_agreement().map_or(false, |h| h == parent_hash) { + return; + } + let authorities = match client.authorities(&BlockId::Hash(parent_hash)) { + Ok(authorities) => authorities, + Err(e) => { + debug!("Error reading authorities: {:?}", e); + return; + } + }; + + let input = Messages { + network_stream: network.bft_messages(parent_hash), + local_id: bft_service.local_id(), + authorities, + }; + + let output = BftSink { network: network, parent_hash: parent_hash, _e: Default::default() }; + match bft_service.build_upon(&header, input.map_err(Into::into), output) { + Ok(Some(bft)) => handle.spawn(bft), + Ok(None) => {}, + Err(e) => debug!(target: "bft", "BFT agreement error: {:?}", e), + } +} +``` +
+启动共识服务,看一下共识服务如何定义: +
+ +``` rust +/// Consensus service. Starts working when created. +//共识服务,多线程并发控制 +pub struct Service { + thread: Option>, + exit_signal: Option<::exit_future::Signal>, +} + +impl Service { + /// Create and start a new instance. + pub fn new( + client: Arc, + api: Arc
, + network: Arc, + transaction_pool: Arc>, + parachain_empty_duration: Duration, + key: ed25519::Pair, + ) -> Service + where + A: LocalPolkadotApi + Send + Sync + 'static, + C: BlockchainEvents + ChainHead + bft::BlockImport + bft::Authorities + Send + Sync + 'static, + { + let (signal, exit) = ::exit_future::signal(); + let thread = thread::spawn(move || { + let mut core = reactor::Core::new().expect("tokio::Core could not be created"); + let key = Arc::new(key); + + let factory = ProposerFactory { + client: api.clone(), + transaction_pool: transaction_pool.clone(), + network: Network(network.clone()), + collators: NoCollators, + parachain_empty_duration, + handle: core.handle(), + }; + let bft_service = Arc::new(BftService::new(client.clone(), key, factory)); + + //网络消息通知 + let notifications = { + let handle = core.handle(); + let network = network.clone(); + let client = client.clone(); + let bft_service = bft_service.clone(); + + client.import_notification_stream().for_each(move |notification| { + if notification.is_new_best { + start_bft(¬ification.header, handle.clone(), &*client, network.clone(), &*bft_service); + } + Ok(()) + }) + }; + + //时间控制 + let interval = reactor::Interval::new_at( + Instant::now() + Duration::from_millis(TIMER_DELAY_MS), + Duration::from_millis(TIMER_INTERVAL_MS), + &core.handle(), + ).expect("it is always possible to create an interval with valid params"); + let mut prev_best = match client.best_block_header() { + Ok(header) => header.blake2_256(), + Err(e) => { + warn!("Cant's start consensus service. Error reading best block header: {:?}", e); + return; + } + }; + + //控制一轮共识的定时器 + let timed = { + let c = client.clone(); + let s = bft_service.clone(); + let n = network.clone(); + let handle = core.handle(); + + interval.map_err(|e| debug!("Timer error: {:?}", e)).for_each(move |_| { + if let Ok(best_block) = c.best_block_header() { + let hash = best_block.blake2_256(); + if hash == prev_best { + debug!("Starting consensus round after a timeout"); + start_bft(&best_block, handle.clone(), &*c, n.clone(), &*s); + } + prev_best = hash; + } + Ok(()) + }) + }; + + //生产相关线程并监听事件 + core.handle().spawn(notifications); + core.handle().spawn(timed); + if let Err(e) = core.run(exit) { + debug!("BFT event loop error {:?}", e); + } + }); + Service { + thread: Some(thread), + exit_signal: Some(signal), + } + } +} + +impl Drop for Service { + fn drop(&mut self) { + if let Some(signal) = self.exit_signal.take() { + signal.fire(); + } + + if let Some(thread) = self.thread.take() { + thread.join().expect("The service thread has panicked"); + } + } +} + +//处理收集的区块和路由信息 +// Collators implementation which never collates anything. +// TODO: do a real implementation. +#[derive(Clone, Copy)] +struct NoCollators; + +impl ::collation::Collators for NoCollators { + type Error = (); + type Collation = future::Empty<::collation::Collation, ()>; + + fn collate(&self, _parachain: ParaId, _relay_parent: Hash) -> Self::Collation { + future::empty() + } + + fn note_bad_collator(&self, _collator: AccountId) { } +} + +type FetchCandidateAdapter = future::Map) -> BlockData>; + +#[derive(Clone)] +struct Router { + network: Arc, +} + +impl Router { + fn fetch_candidate_adapter(data: Vec) -> BlockData { + BlockData(data) + } +} + +impl TableRouter for Router { + type Error = Canceled; + type FetchCandidate = FetchCandidateAdapter; + type FetchExtrinsic = future::FutureResult; + + fn local_candidate_data(&self, hash: Hash, block_data: BlockData, _extrinsic: Extrinsic) { + let data = block_data.0; + self.network.set_local_candidate(Some((hash, data))) + } + + fn fetch_block_data(&self, candidate: &CandidateReceipt) -> Self::FetchCandidate { + let hash = candidate.hash(); + self.network.fetch_candidate(&hash).map(Self::fetch_candidate_adapter) + } + + fn fetch_extrinsic_data(&self, _candidate: &CandidateReceipt) -> Self::FetchExtrinsic { + future::ok(Extrinsic) + } +} +``` +
+在一个循环里不断的收集: +
+ +``` rust +impl Future for CollationFetch { + type Item = (Collation, Extrinsic); + type Error = C::Error; + + fn poll(&mut self) -> Poll<(Collation, Extrinsic), C::Error> { + let parachain = match self.parachain.as_ref() { + Some(p) => p.clone(), + None => return Ok(Async::NotReady), + }; + + loop { + let x = { + let (r, c) = (self.relay_parent_hash, &self.collators); + let poll = self.live_fetch + .get_or_insert_with(move || c.collate(parachain, r).into_future()) + .poll(); + + if let Err(_) = poll { self.parachain = None } + try_ready!(poll) + }; + + match validate_collation(&*self.client, &self.relay_parent, &x) { + Ok(()) => { + self.parachain = None; + + // TODO: generate extrinsic while verifying. + return Ok(Async::Ready((x, Extrinsic))); + } + Err(e) => { + debug!("Failed to validate parachain due to API error: {}", e); + + // just continue if we got a bad collation or failed to validate + self.live_fetch = None; + self.collators.note_bad_collator(x.receipt.collator) + } + } + } + } +``` +
+处理提议,并达成共识: +
+ +``` rust +/// Import a single statement. Provide a handle to a table router +/// for dispatching any other requests which come up. +pub fn import_statement bool>( + &self, + router: &R, + statement: table::SignedStatement, + received_from: StatementSource, + check_candidate: C, +) -> StatementProducer<::Future, ::Future, C> { + self.inner.lock().import_statement(&*self.context, router, statement, received_from, check_candidate) +} + +/// Sign and import a local statement. +pub fn sign_and_import( + &self, + router: &R, + statement: table::Statement, +) { + let proposed_digest = match statement { + GenericStatement::Candidate(ref c) => Some(c.hash()), + _ => None, + }; + + let signed_statement = self.context.sign_statement(statement); + + let mut inner = self.inner.lock(); + if proposed_digest.is_some() { + inner.proposed_digest = proposed_digest; + } + + let producer = inner.import_statement( + &*self.context, + router, + signed_statement, + StatementSource::Local, + |_| true, + ); + + assert!(producer.work.is_none(), "local statement import never leads to additional work; qed"); +} + +/// Import many statements at once. +/// +/// Provide an iterator yielding pairs of (statement, statement_source). +pub fn import_statements(&self, router: &R, iterable: I) -> U + where + R: TableRouter, + I: IntoIterator, + C: FnMut(Collation) -> bool, + U: ::std::iter::FromIterator::Future, + ::Future, + C, + >>, +{ + let mut inner = self.inner.lock(); + + iterable.into_iter().map(move |(statement, statement_source, check_candidate)| { + inner.import_statement(&*self.context, router, statement, statement_source, check_candidate) + }).collect() +} + +/// Execute a closure using a specific candidate. +/// +/// Deadlocks if called recursively. +pub fn with_candidate(&self, digest: &Hash, f: F) -> U + where F: FnOnce(Option<&CandidateReceipt>) -> U +{ + let inner = self.inner.lock(); + f(inner.table.get_candidate(digest)) +} + +/// Execute a closure using the current proposed set. +/// +/// Deadlocks if called recursively. +pub fn with_proposal(&self, f: F) -> U + where F: FnOnce(Vec<&CandidateReceipt>) -> U +{ + let inner = self.inner.lock(); + f(inner.table.proposed_candidates(&*self.context)) +} + +/// Get the number of parachains which have available candidates. +pub fn includable_count(&self) -> usize { + self.inner.lock().table.includable_count() +} + +/// Get all witnessed misbehavior. +pub fn get_misbehavior(&self) -> HashMap { + self.inner.lock().table.get_misbehavior().clone() +} + +/// Fill a statement batch. +pub fn fill_batch(&self, batch: &mut B) { + self.inner.lock().table.fill_batch(batch); +} + +/// Track includability of a given set of candidate hashes. +pub fn track_includability(&self, iterable: I) -> Includable + where I: IntoIterator +{ + let mut inner = self.inner.lock(); + + let (tx, rx) = includable::track(iterable.into_iter().map(|x| { + let includable = inner.table.candidate_includable(&x, &*self.context); + (x, includable) + })); + + if !tx.is_complete() { + inner.trackers.push(tx); + } + + rx +} +} +``` +
+最终的BFT处理: +
+ +``` rust +/// Actions which can be taken during the BFT process. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum Action { + /// Proposal of a block candidate. + Propose(u32, Block), + /// Proposal header of a block candidate. Accompanies any proposal, + /// but is used for misbehavior reporting since blocks themselves are big. + ProposeHeader(u32, HeaderHash), + /// Preparation to commit for a candidate. + Prepare(u32, HeaderHash), + /// Vote to commit to a candidate. + Commit(u32, HeaderHash), + /// Vote to advance round after inactive primary. + AdvanceRound(u32), +} + +impl Slicable for Action { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + match *self { + Action::Propose(ref round, ref block) => { + v.push(ActionKind::Propose as u8); + round.using_encoded(|s| v.extend(s)); + block.using_encoded(|s| v.extend(s)); + } + Action::ProposeHeader(ref round, ref hash) => { + v.push(ActionKind::ProposeHeader as u8); + round.using_encoded(|s| v.extend(s)); + hash.using_encoded(|s| v.extend(s)); + } + Action::Prepare(ref round, ref hash) => { + v.push(ActionKind::Prepare as u8); + round.using_encoded(|s| v.extend(s)); + hash.using_encoded(|s| v.extend(s)); + } + Action::Commit(ref round, ref hash) => { + v.push(ActionKind::Commit as u8); + round.using_encoded(|s| v.extend(s)); + hash.using_encoded(|s| v.extend(s)); + } + Action::AdvanceRound(ref round) => { + v.push(ActionKind::AdvanceRound as u8); + round.using_encoded(|s| v.extend(s)); + } + } + + v + } + + fn decode(value: &mut I) -> Option { + match i8::decode(value) { + Some(x) if x == ActionKind::Propose as i8 => { + let (round, block) = try_opt!(Slicable::decode(value)); + Some(Action::Propose(round, block)) + } + Some(x) if x == ActionKind::ProposeHeader as i8 => { + let (round, hash) = try_opt!(Slicable::decode(value)); + + Some(Action::ProposeHeader(round, hash)) + } + Some(x) if x == ActionKind::Prepare as i8 => { + let (round, hash) = try_opt!(Slicable::decode(value)); + + Some(Action::Prepare(round, hash)) + } + Some(x) if x == ActionKind::Commit as i8 => { + let (round, hash) = try_opt!(Slicable::decode(value)); + + Some(Action::Commit(round, hash)) + } + Some(x) if x == ActionKind::AdvanceRound as i8 => { + Slicable::decode(value).map(Action::AdvanceRound) + } + _ => None, + } + } +} + +/// Messages exchanged between participants in the BFT consensus. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Message { + /// The parent header hash this action is relative to. + pub parent: HeaderHash, + /// The action being broadcasted. + pub action: Action, +} + +impl Slicable for Message { + fn encode(&self) -> Vec { + let mut v = self.parent.encode(); + self.action.using_encoded(|s| v.extend(s)); + v + } + + fn decode(value: &mut I) -> Option { + Some(Message { + parent: try_opt!(Slicable::decode(value)), + action: try_opt!(Slicable::decode(value)), + }) + } +} + +/// Justification of a block. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct Justification { + /// The round consensus was reached in. + pub round_number: u32, + /// The hash of the header justified. + pub hash: HeaderHash, + /// The signatures and signers of the hash. + pub signatures: Vec<(::AuthorityId, ::Signature)> +} + +impl Slicable for Justification { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + + self.round_number.using_encoded(|s| v.extend(s)); + self.hash.using_encoded(|s| v.extend(s)); + self.signatures.using_encoded(|s| v.extend(s)); + + v + } + + fn decode(value: &mut I) -> Option { + Some(Justification { + round_number: try_opt!(Slicable::decode(value)), + hash: try_opt!(Slicable::decode(value)), + signatures: try_opt!(Slicable::decode(value)), + }) + } +} + +// single-byte code to represent misbehavior kind. +#[repr(i8)] +enum MisbehaviorCode { + /// BFT: double prepare. + BftDoublePrepare = 0x11, + /// BFT: double commit. + BftDoubleCommit = 0x12, +} + +impl MisbehaviorCode { + fn from_i8(x: i8) -> Option { + match x { + 0x11 => Some(MisbehaviorCode::BftDoublePrepare), + 0x12 => Some(MisbehaviorCode::BftDoubleCommit), + _ => None, + } + } +} + +/// Misbehavior kinds. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub enum MisbehaviorKind { + /// BFT: double prepare. + BftDoublePrepare(u32, (HeaderHash, Signature), (HeaderHash, Signature)), + /// BFT: double commit. + BftDoubleCommit(u32, (HeaderHash, Signature), (HeaderHash, Signature)), +} + +/// A report of misbehavior by an authority. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct MisbehaviorReport { + /// The parent hash of the block where the misbehavior occurred. + pub parent_hash: HeaderHash, + /// The parent number of the block where the misbehavior occurred. + pub parent_number: ::block::Number, + /// The authority who misbehavior. + pub target: AuthorityId, + /// The misbehavior kind. + pub misbehavior: MisbehaviorKind, +} + +impl Slicable for MisbehaviorReport { + fn encode(&self) -> Vec { + let mut v = Vec::new(); + self.parent_hash.using_encoded(|s| v.extend(s)); + self.parent_number.using_encoded(|s| v.extend(s)); + self.target.using_encoded(|s| v.extend(s)); + + match self.misbehavior { + MisbehaviorKind::BftDoublePrepare(ref round, (ref h_a, ref s_a), (ref h_b, ref s_b)) => { + (MisbehaviorCode::BftDoublePrepare as i8).using_encoded(|s| v.extend(s)); + round.using_encoded(|s| v.extend(s)); + h_a.using_encoded(|s| v.extend(s)); + s_a.using_encoded(|s| v.extend(s)); + h_b.using_encoded(|s| v.extend(s)); + s_b.using_encoded(|s| v.extend(s)); + } + MisbehaviorKind::BftDoubleCommit(ref round, (ref h_a, ref s_a), (ref h_b, ref s_b)) => { + (MisbehaviorCode::BftDoubleCommit as i8).using_encoded(|s| v.extend(s)); + round.using_encoded(|s| v.extend(s)); + h_a.using_encoded(|s| v.extend(s)); + s_a.using_encoded(|s| v.extend(s)); + h_b.using_encoded(|s| v.extend(s)); + s_b.using_encoded(|s| v.extend(s)); + } + } + + v + } + + fn decode(input: &mut I) -> Option { + let parent_hash = HeaderHash::decode(input)?; + let parent_number = ::block::Number::decode(input)?; + let target = AuthorityId::decode(input)?; + + let misbehavior = match i8::decode(input).and_then(MisbehaviorCode::from_i8)? { + MisbehaviorCode::BftDoublePrepare => { + MisbehaviorKind::BftDoublePrepare( + u32::decode(input)?, + (HeaderHash::decode(input)?, Signature::decode(input)?), + (HeaderHash::decode(input)?, Signature::decode(input)?), + ) + } + MisbehaviorCode::BftDoubleCommit => { + MisbehaviorKind::BftDoubleCommit( + u32::decode(input)?, + (HeaderHash::decode(input)?, Signature::decode(input)?), + (HeaderHash::decode(input)?, Signature::decode(input)?), + ) + } + }; + + Some(MisbehaviorReport { + parent_hash, + parent_number, + target, + misbehavior, + }) + } +} +``` +
+波卡链的共识没有特别之处,它的难点还在于Router的控制和对链的进入、出口的管理。通过注册机制可以较好的控制这些,但是缺点就在于,会开成中心机制(因为不可能向链数量扩展,否则就是一条链了,必然是代表节点,最终形成类似于EOS的超级节点)。 +
diff --git "a/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\344\272\244\346\230\223.md" "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\344\272\244\346\230\223.md" new file mode 100644 index 0000000..ab56022 --- /dev/null +++ "b/polkadot/\346\263\242\345\215\241\351\223\276\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213\345\233\233\344\272\244\346\230\223.md" @@ -0,0 +1,334 @@ + +# 波卡链源码分析之四交易 + +
+ +## 一、交易的发起 +
+交易的发起从客户端的RPC发起,在波卡链中,把这个称做外部交易。外部交易将被RPC打包到交易池中,代码如下: +
+ +``` rust +struct RpcTransactionPool { + inner: Arc>, + network: Arc, +} +//这里使用并实现了trait +impl substrate_rpc::author::AuthorApi for RpcTransactionPool { + fn submit_extrinsic(&self, xt: Extrinsic) -> substrate_rpc::author::error::Result<()> { + use primitives::hexdisplay::HexDisplay; + use polkadot_runtime::UncheckedExtrinsic; + use codec::Slicable; + + info!("Extrinsic submitted: {}", HexDisplay::from(&xt.0)); + //通过XT交易来进行转换,这个在以后会根据情况进行处理。 + let decoded = xt.using_encoded(|ref mut s| UncheckedExtrinsic::decode(s)) + .ok_or(substrate_rpc::author::error::ErrorKind::InvalidFormat)?; + + info!("Correctly formatted: {:?}", decoded); + + //导入到交易池 + self.inner.lock().import(decoded) + .map_err(|_| substrate_rpc::author::error::ErrorKind::PoolError)?; + + self.network.trigger_repropagate(); + Ok(()) + } +} +/// Called when new transactons are imported by the client. +pub fn trigger_repropagate(&self) { + self.network.with_context(DOT_PROTOCOL_ID, |context| { + //分发交易 + self.handler.protocol.propagate_transactions(&mut NetSyncIo::new(context)); + }); +} +/// Called when we propagate ready transactions to peers. +pub fn propagate_transactions(&self, io: &mut SyncIo) { + debug!(target: "sync", "Propagating transactions"); + + // Accept transactions only when fully synced + if self.sync.read().status().state != SyncState::Idle { + return; + } + + let transactions = self.transaction_pool.transactions(); + + //得到可发送节点 + let mut peers = self.peers.write(); + for (peer_id, ref mut peer) in peers.iter_mut() { + let to_send: Vec<_> = transactions.iter().filter_map(|&(hash, ref t)| + if peer.known_transactions.insert(hash.clone()) { Some(t.clone()) } else { None }).collect(); + if !to_send.is_empty() { + trace!(target: "sync", "Sending {} transactions to {}", to_send.len(), peer_id); + //发送交易 + self.send_message(io, *peer_id, Message::Transactions(to_send)); + } + } +} +``` +
+trigger_repropagate函数的注释说得很清楚,当一个新的交易被客户端导入时,触发这个函数。propagate_transactions这个函数(propagate有繁殖扩散的意思,很贴切)会判断一下节点的状态,如果非空闲就直接返回(基本上比特币和以太坊也是这个调调)。否则根据节点中的状态发送得到的交易。 +
+传播交易其实是非常重要的一步,这也是Service.rs这个文件的重要一个功能。 +
+ 回到交易池,在交易池的代码(polkadot\transaction_pool\src\lib.rs)中的库的引用中有下面一段代码: +
+pub use transaction_pool::{Options, Status, LightStatus, NoopListener, VerifiedTransaction as VerifiedTransactionOps}; +
+也即是说,把几种状态(轻节点,校验等)都引入了进来。在这个池子的代码中,区块和区块产生也包含在内了,先重点分析一下交易池: +
+ +``` rust +/// The polkadot transaction pool. +/// +/// Wraps a `transaction-pool::Pool`. +pub struct TransactionPool { + inner: transaction_pool::Pool, + insertion_index: u64, // TODO: use AtomicU64 when it stabilizes +} + +impl TransactionPool { + /// Create a new transaction pool. + pub fn new(options: Options) -> Self { + TransactionPool { + inner: Pool::new(NoopListener, Scoring, options), + insertion_index: 0, + } + } + + /// Verify and import a transaction into the pool.这里将交易导入交易池并做检验 + pub fn import(&mut self, xt: UncheckedExtrinsic) -> Result> { + let insertion_index = self.insertion_index; + self.insertion_index += 1; + + //生成一个检查器 + let verified = VerifiedTransaction::create(xt, insertion_index)?; + + info!("Extrinsic verified {:?}. Importing...", verified); + + // TODO: just use a foreign link when the error type is made public. + //将错误保存 + let hash = verified.hash.clone(); + self.inner.import(verified) + .map_err(|e| + match e { + // TODO: make error types public in transaction_pool. For now just treat all errors as AlreadyImported + _ => ErrorKind::AlreadyImported(hash), + // transaction_pool::error::AlreadyImported(h) => ErrorKind::AlreadyImported(h), + // e => ErrorKind::Import(Box::new(e)), + }) + .map_err(Into::into) + } + //它们都在同一个文件中 + /// A verified transaction which should be includable and non-inherent. +#[derive(Debug, Clone)] +pub struct VerifiedTransaction { + inner: ::Checked,//未检查的外部交易状态 + hash: Hash, + address: AccountId, + insertion_id: u64, + encoded_size: usize, +} + +impl VerifiedTransaction { + /// Attempt to verify a transaction. + fn create(xt: UncheckedExtrinsic, insertion_id: u64) -> Result { + if !xt.is_signed() { + bail!(ErrorKind::IsInherent(xt)) + } + + let message = codec::Slicable::encode(&xt);//处理消息数据 + //熟悉的switch + match xt.check() { + Ok(xt) => { + // TODO: make transaction-pool use generic types. + let hash = substrate_primitives::hashing::blake2_256(&message); + let address = xt.signed; + Ok(VerifiedTransaction { + inner: xt, + hash: hash.into(), + encoded_size: message.len(), + address, + insertion_id, + }) + } + Err(xt) => Err(ErrorKind::BadSignature(xt).into()), + } + } +``` +
+经过检验后的交易就将其返回,准备打包到区块上。 +
+
+ +# 二、交易转发 +
+前面看到了分发的代码,接着分析具体的调用代码: +
+ +``` rust +pub fn send_message(&self, io: &mut SyncIo, peer_id: PeerId, mut message: Message) { + match &mut message { + &mut Message::BlockRequest(ref mut r) => { + let mut peers = self.peers.write(); + if let Some(ref mut peer) = peers.get_mut(&peer_id) { + r.id = peer.next_request_id; + peer.next_request_id = peer.next_request_id + 1; + peer.block_request = Some(r.clone()); + peer.request_timestamp = Some(time::Instant::now()); + } + }, + _ => (), //否则默认返回一个空元组 + } + let data = serde_json::to_vec(&message).expect("Serializer is infallible; qed"); + if let Err(e) = io.send(peer_id, data) {//调用 IO发送 + debug!(target:"sync", "Error sending message: {:?}", e); + io.disconnect_peer(peer_id); + } +} +//network/src/io.rs +fn send(&mut self, peer_id: PeerId, data: Vec) -> Result<(), NetworkError>{ + self.network.send(peer_id, 0, data) +} +``` +
+上面的代码把字符串序列化后,通过IO发送到网络接口上。这里的网络接口的库包没找到,不知道是原生库的还是自己写的。 +
+ +# 三、交易的打包和上链 +
+交易的打包上链在波卡链上比较麻烦,它主要分成两部分,也就是说一个是平行链上的打包交易,然后生成队列再发送到中继链上进行打包交易确认。也就是说,它是分成两部分的。 +
+在波卡链上,块的生成是通过共识来产生的。平行链和中继链的块产生没有本质区别,区块产生在全节点上,轻节点一般只是用来做验证(polkadot/api/src/full.rs)。为了能够更清晰的看出区块产生的过程,先看一下它的测试函数: +
+ +``` rust +#[test] +fn build_block() { + let client = client(); + + let id = client.check_id(BlockId::Number(0)).unwrap(); + let block_builder = client.build_block(&id, 1_000_000, Vec::new()).unwrap(); + let block = block_builder.bake(); + + assert_eq!(block.header.number, 1); + assert!(block.header.extrinsics_root != Default::default()); +} +``` +
+它其实只调用了ID检查,创建区块和区块头的检验。实际的产生应该和此类似。 +
+ +``` rust +fn propose_with(&self, candidates: Vec) -> Result { + // TODO: handle case when current timestamp behind that in state. + let timestamp = current_timestamp(); + let mut block_builder = self.client.build_block( + &self.parent_id, + timestamp, + candidates, + )?; + + let readiness_evaluator = Ready::create(self.parent_id.clone(), &*self.client); + + { + let mut pool = self.transaction_pool.lock(); + let mut unqueue_invalid = Vec::new(); + let mut pending_size = 0; + + pool.cull(None, readiness_evaluator.clone()); + for pending in pool.pending(readiness_evaluator.clone()) { + // skip and cull transactions which are too large. + if pending.encoded_size() > MAX_TRANSACTIONS_SIZE { + unqueue_invalid.push(pending.hash().clone()); + continue + } + + if pending_size + pending.encoded_size() >= MAX_TRANSACTIONS_SIZE { break } + + match block_builder.push_extrinsic(pending.as_transaction().clone()) { + Ok(()) => { + pending_size += pending.encoded_size(); + } + Err(e) => { + trace!(target: "transaction-pool", "Invalid transaction: {}", e); + unqueue_invalid.push(pending.hash().clone()); + } + } + } + + for tx_hash in unqueue_invalid { + pool.remove(&tx_hash, false); + } + } + + let polkadot_block = block_builder.bake(); + + info!("Proposing block [number: {}; hash: {}; parent_hash: {}; extrinsics: [{}]]", + polkadot_block.header.number, + Hash::from(polkadot_block.header.blake2_256()), + polkadot_block.header.parent_hash, + polkadot_block.extrinsics.iter() + .map(|xt| format!("{}", Hash::from(xt.blake2_256()))) + .collect::>() + .join(", ") + ); + + let substrate_block = Slicable::decode(&mut polkadot_block.encode().as_slice()) + .expect("polkadot blocks defined to serialize to substrate blocks correctly; qed"); + + // TODO: full re-evaluation + let active_parachains = self.client.active_parachains(&self.parent_id)?; + assert!(evaluation::evaluate_initial( + &substrate_block, + timestamp, + &self.parent_hash, + self.parent_number, + &active_parachains, + ).is_ok()); + + Ok(substrate_block) + } + +//这里调用创建区块部分 +impl Future for CreateProposal + where + C: PolkadotApi, + R: TableRouter, + P: Collators, +{ + type Item = SubstrateBlock; + type Error = Error; + + fn poll(&mut self) -> Poll { + // 1. poll local collation future. + match self.collation.poll() { + Ok(Async::Ready((collation, extrinsic))) => { + let hash = collation.receipt.hash(); + self.router.local_candidate_data(hash, collation.block_data, extrinsic); + + // TODO: if we are an availability guarantor also, we should produce an availability statement. + self.table.sign_and_import(&self.router, GenericStatement::Candidate(collation.receipt)); + } + Ok(Async::NotReady) => {}, + Err(_) => {}, // TODO: handle this failure to collate. + } + + // 2. try to propose if we have enough includable candidates and other + // delays have concluded. + let included = self.table.includable_count(); + try_ready!(self.timing.poll(included)); + + // 3. propose这里调用区块的生产 + let proposed_candidates = self.table.with_proposal(|proposed_set| { + proposed_set.into_iter().cloned().collect() + }); + + self.propose_with(proposed_candidates).map(Async::Ready) + } +} +``` +
+这样一个区块就把交易打包好了,下一步就是对区块进行共识。 +
+
diff --git a/privacy/img/MLSAG_01.png b/privacy/img/MLSAG_01.png new file mode 100644 index 0000000..84cdf23 Binary files /dev/null and b/privacy/img/MLSAG_01.png differ diff --git a/privacy/img/MLSAG_02.png b/privacy/img/MLSAG_02.png new file mode 100644 index 0000000..29ecff2 Binary files /dev/null and b/privacy/img/MLSAG_02.png differ diff --git a/privacy/img/MLSAG_03.png b/privacy/img/MLSAG_03.png new file mode 100644 index 0000000..b162457 Binary files /dev/null and b/privacy/img/MLSAG_03.png differ diff --git a/privacy/img/MLSAG_04.png b/privacy/img/MLSAG_04.png new file mode 100644 index 0000000..59a3ebb Binary files /dev/null and b/privacy/img/MLSAG_04.png differ diff --git a/privacy/img/dash-join.png b/privacy/img/dash-join.png new file mode 100644 index 0000000..211ed90 Binary files /dev/null and b/privacy/img/dash-join.png differ diff --git "a/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\200\346\200\273\344\275\223\345\257\271\346\257\224.md" "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\200\346\200\273\344\275\223\345\257\271\346\257\224.md" new file mode 100644 index 0000000..599e7d5 --- /dev/null +++ "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\200\346\200\273\344\275\223\345\257\271\346\257\224.md" @@ -0,0 +1,99 @@ +# 门罗币、零币和达世币的对比分析之一总体对比 + +## 一、目前匿名加密货币的情况 +区块链的一个重要特点是公开性,也就是说,任何人都可以从链上把整个区块的数据拉取下来,虽然包括比特币等区块链项目都采用的匿名交易,但是通过对数据地址和相关IP的分析,仍然大概率发现交易的双方。而相当一部分交易是不需要或者说不想让别人知道交易的细节的。特别是交易的对象。因此,一些新的增加了隐私性的区块链就应运而生,其中,比较有特点有以下几个: +
+1、门罗币:门罗币主要是是采用签名来保护隐私性。 +
+2、零币:人们觉得不好听,叫ZEC,这个不重要。它主要使用snark库来实现零知识明,达到保护隐私的目的。 +
+3、达世币,也就是原来的暗黑币,听这名字就知道这个是什么人在用。肯定隐私保护的好,他使用的是混币技术。 +
+其它比如PIVX、CLOAK (斗篷币)以及(CoinSuffle)NXT就不再一一对比分析,有兴趣可以查看相关技术资料。 + +## 二、门罗币(monero) + +门罗使用的算法是基于Crypto Note协议的Crypto Night算法,最初是为了抵抗类似比特币的矿机而设计的。但后来还是出现了类似的矿机。 +
+门罗币使用的是环签名,那么什么是环签名呢?要想明白环签名,首先要搞明白群签名。 +
+群签名的特点:只有群中成员能够代表群体签名(群特性);接收者可以用公钥验证群签名(验证简单性);接收者不能知道由群体中哪个成员所签(无条件匿名保护);发生争议时,群体中的成员或可信赖机构可以识别签名者(可追查性)。更多细节可以查看维基百科: +
+http://en.wikipedia.org/wiki/Group_signature +
+环签名:签名者首先选定一个临时的签名者集合,集合中包括签名者自身。然后签名者利用自己的私钥和签名集合中其他人的公钥就可以独立的产生签名,而无需他人的帮助。签名者集合中的其他成员可能并不知道自己被包含在其中。 +
+二者的区别在于: +
+1、群签名可以追踪签名者而环签名还情况下不可以。 +
+2、群签名有管理员,而环签名不需要。 +
+更多的数学定义请自行查阅相关资料。 +
+环签名主要有四大类: +
+1、门限环签名 +
+2、关联环签名 +
+3、可撤销匿名性的环签名 +
+4、可否认的环签名 +
+在2017年9月,门罗币每六个月一次的硬分叉升级上使用了环形机密交易(Ring Confidential Transactions)算法,它更好的保护了隐私性。在后面的源码分析中,会针对具体的加密细节进行阐述。 + +## 三、零币ZEC(ZCash) +ZCash是基于比特币0.11.2开发的,二者的最主要的不同就在于隐私性的保护。前者使用了零知识证明算法zk-SNARKS。 +
+简单的来说,零知识证明就是证明者能够在不向验证者提供任何有用的信息的情况下,使验证者相信某个论断是正确的。网上有几个经典的例子,最典型的就是大家熟悉的阿里巴巴和四十大盗的开门问题。 +
+ZEC的应用原理是把有的币匿名集中起来,在主链上(zerocoin就是在比特币的主链外增加了一条辅助的侧链,而Zcash是在Zerocoin基础上)进行一种类似于混币的过程,这样就完全不知道交易的信息了。 +
+ZEC的交易有三种方式: +
+1、透明资金发送到透明地址,使用bitcoin的交易方式。 +2、透明资金发送到透明地址、私有地址,透明资金发送到透明地址,使用bitcoin的方式,发送到私有地址的需要构造证明,然后对整个交易签名才能发送。 +3、私有资金发送到透明地址、私有地址,每笔交易需要生成证明,涉及到找零、矿工费的处理,最后再签名发送。 +
+ZEC的隐藏部分很少,只有大约十分之一,原因就是前面提到的,那个零知识证明是非常耗时的,所以大多数不必要的交易就直接透明传输了。不过所国外的网站上说,好像zk-SNARKS算法并没有经过严格的学术测试,换句话说,这东西太新,到底有多靠谱还需要时间来检验。 +
+比如有人就提到,zcash存在trust setup 缺陷, 也就是通过该缺陷产生的随机种子, 可以跟踪货币的接受和发送者。 +
+ +## 四、达世币DASH(原来的暗黑币) + +达世币也有三种转账方式: +
+1、是像比特币一样的普通转账。 +
+2、是即时交易。不需要矿工打包确认,就可以确认交易,几乎可以实现秒到。 +
+3、是匿名交易。从区块链上看不到是谁和谁进行了转账。 +
+达世币如何进行匿名交易呢?达世币中除了普通节点之外,还有一种节点叫“主节点”。主节点可以提供一系列服务,如:匿名交易和即时支付。如果有用户发起了匿名交易,它的申请会发送到相关的主节点上,然后主节点根据情况,把多个申请者的交易进行混合,也就是混币(一般来说,是三笔交易一起进行。)网上有一个例子,一桌人把自己的钱都放在桌上,混在一起,然后再分别拿回相应面值的钱,这样就不知道你手里的钱到底是谁的了,这就是混币。混币后,网络就不知道究竟谁转账给了谁。 +
+达世币的优势在于,它可以比较容易的实现隐私性,不需要额外的新的算法,即使在混币的过程中会有性能损失,从理论上讲还是可以忍受的。 +
+ +## 五、对比 + +
+零知识证明的优越性是无可比拟的,但是,它的明显的缺点是没经过长时间的检测,运行速度太慢,即使经过不断的优化,其运行的时间仍然维持在了将近十秒的左右。这个就大规模推广使用而言,仍然有很远的距离。 +
+另外,它只能处理固定面值的交易,交易的数据量也比较大(25K),而且它的匿名性也使其不好监管,产生的BUG就是,黑客可以任意的创造币,导致发行量的巨额增加。 +
+环签名的缺点是,仍然有办法被攻破,而导致隐私交易达不到隐私的目的。在相关资料上,有人提问,经过环签名的隐私保护程度如何?答案是“依赖于所有参与此次环签名的钥匙(key)都不可追踪。”换句话说,有一个签名的私钥可追踪,那么就会使隐私性受到威胁。 +
+另外CoyptoNote本身是有漏洞的,门罗币曾经遭到过一次子集网络划分的攻击。 +
+达世币的混币池交易,从理论上讲是安全的,但实际上,混币的过程也有很多漏洞,首先,混币为了能够更好的隐藏交易,一般是中心化部署的,很容易受到攻击,而且混币的主节点如果能够做恶,那啥也别说了。 +
+另外,如果用户在交易时没有及时清理Cookie,还是可能通过技术手段寻找到相关的信息的。 +
+ +## 六、总结 +总之,在区块链上搞隐私的方法现在是百花齐放,从各个角度各个层面都有切入的,但是匿名性必然会增加链的负担,所以需要在二者之间寻找一个动态的平衡,既要保证交易的可持续,又要隐私的安全性。 +
+这三种区块链进行了有益的探索,但是从实用的角度看,还有相当的不足,仍然需要进一步的完善和发展。 +
diff --git "a/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\211\350\276\276\344\270\226\345\270\201\347\232\204\346\267\267\345\270\201\345\210\206\346\236\220.md" "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\211\350\276\276\344\270\226\345\270\201\347\232\204\346\267\267\345\270\201\345\210\206\346\236\220.md" new file mode 100644 index 0000000..58c93e8 --- /dev/null +++ "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\270\211\350\276\276\344\270\226\345\270\201\347\232\204\346\267\267\345\270\201\345\210\206\346\236\220.md" @@ -0,0 +1,395 @@ +# 门罗币、ZEC和达世币的对比分析之三达世币的混币分析 + +## 一、达世币的匿名特点 +达世币也就是原来的暗黑币,估计是觉着不太正能量,换名字了。暗黑币从一出世其实就在一些见不得人的网站上使用量非常频繁,原因大家都知道。也就是今天要分析的匿名性。 +
+达世币的匿名性保证了买卖双方的不可见(一般情况),而它使用混币池技术来实现匿名性也算是一个不错的尝试。混币池优点是如果混合的数量足够多,那么苏匿名的安全性还是很高的, 缺点是主节点有中心化的趋势。 +
+达世几点使用了X11的算法,什么叫X11的算法呢?就是11种加密算法混合到一起的算法,真是不嫌麻烦啊。比如有SHA256,有抵抗矿机的Scrypt等等,目的很简单,也是为了抵抗专门的挖矿矿机的迅速产生。 +
+做为比较早期的匿名性引入的数字币,它从技术角度上来看,还是不太完美的。匿名只能算是它的一个方面,而不是全部。 + +## 二、混币的原理 +在达世币中,大约具有2400个生效的主节点,由于它使用了和比特币类似的机制,所以需要掌握大约百分之五十的算力才攻击整个网络,而每个主节点大约需要1000 DASH担保。如果想控制整个网络还是需要很大的代价的。 +
+因此,可以使用去中心化的混币服务(从这方面讲确实是比EOS的超级节点要多很多)。大家都知道,在比特币的网络中,理论上也是匿名的,因为你并被有注册你的真实的身份到区块链的网络上去,在交易中只是可以看到交易的哈希或者说相关的地址。 +
+但是,如果利用大数据把与此地址相关的所有的交易进行关联、分析。就可以分析出交易发出的IP、端口,坐落的城市。理论上讲,如果使用者没有对IP进行保护的话,再通过分析此IP的相关的一些数据信息,比如此IP和其它网站通信时使用的电话号码等信息,就可以很容易的找到交易的双方。 +
+在Dash币的白皮书上,介绍了简单的比特币的混币和coinjoin技术,它们都有着可以在某个点被破解的较大可能。而达世币的PrivateSend则可以避免上述两者的问题。 +
+达世币的想法是如果把多个交易混合成一笔交易,那么拆分这笔交易就是一个难题了。在数学上,因式分解是不确定的,交易混合的越多,那么出现的分解的可能性越多。这样就避免上被按照交易回溯用户的信息了。 +
+ +## 三、达世币中对混币的应用 +在达世币中,最少要使用三个用户的资金进行混币,下图是三个用户的资金合并到一个共同交易,用户会以新的打乱过的形式对外输出资金。 +
+ +![dashjoin](img/dash-join.png) +
+如果仅仅是做上述的动作,那么仍然有三分之一的机会可以追踪使用原来的方式查的交易的双方。为了解决这个问题,达世币进行了进一步的处理,首先,它把混合的交易链接,然后由不同的主机节分别发送,这样就无法判定到底是哪个主节点发送,进一步增加查看的难度。然后,它还使用中继遮掩技术,让发送者将交易一开始不提交到交易池中,而是由随机指定的主节点输入输出转发到相关的目标节点上。 +
+在达世币中,每轮混币的总额不超过1000个DASH且需要多轮才能达到混币的输出。为了防止攻击,达世币是以押金的方式向矿池提交交易,这样就增加了攻击的成本。 +
+在达世币的白皮书中,有各种情况下难度的概率表,可以参看。 + +## 四、源码分析 +这里主要分析privatesend的相关代码,看一下数据结构: + +```C++ +//混币的交易池看的输入,继承自比特币的普通输入交易 +class CTxDSIn : public CTxIn +{ +public: + // memory only + CScript prevPubKey; + bool fHasSig; // flag to indicate if signed + int nSentTimes; //times we've sent this anonymously +...... +}; + +// 混币池的记录(条目)数据结构 +class CDarkSendEntry +{ +public: + //下面两个混币的输入输出数据结构 + std::vector vecTxDSIn; + std::vector vecTxOut; + CTransaction txCollateral; + // memory only + CService addr; +....... + + bool AddScriptSig(const CTxIn& txin); +}; +//正在进行的混混币的归并和相关面额信息 +class CDarksendQueue +{ +public: + int nDenom; + CTxIn vin; + int64_t nTime; + bool fReady; //ready for submit + std::vector vchSig; + // memory only + bool fTried; + +...... +}; + +/** 混币交易的帮助类 + */ +class CDarksendBroadcastTx +{ +private: + // memory only + // when corresponding tx is 0-confirmed or conflicted, nConfirmedHeight is -1 + int nConfirmedHeight; + +public: + CTransaction tx; + CTxIn vin; + std::vector vchSig; + int64_t sigTime; +...... +}; +``` +
+上面是基本的几个数据结构,下面看一下比较主要的两个类,一个是控制私有发送交易的基础类,一个是私有发送类: + +```C++ +//这个类是为后面的Client和Server准备的 +class CPrivateSendBase +{ +protected: + mutable CCriticalSection cs_darksend; + + //当前混币的存储数据结构 + std::vector vecDarksendQueue; + //主节点和客户端的条目数据结构 + std::vector vecEntries; // Masternode/clients entries + + PoolState nState; // should be one of the POOL_STATE_XXX values + int64_t nTimeLastSuccessfulStep; // the time when last successful mixing step was performed, in UTC milliseconds + + int nSessionID; // 0 if no mixing session is active + + CMutableTransaction finalMutableTransaction; // the finalized transaction ready for signing + + void SetNull(); + void CheckQueue(); + +public: + int nSessionDenom; //Users must submit an denom matching this + + CPrivateSendBase() { SetNull(); } + + int GetQueueSize() const { return vecDarksendQueue.size(); } + int GetState() const { return nState; } + std::string GetStateString() const; + + int GetEntriesCount() const { return vecEntries.size(); } +}; + +class CPrivateSend +{ +private: +...... + static const CAmount COLLATERAL = 0.001 * COIN; + + // static members + static std::vector vecStandardDenominations; + static std::map mapDSTX; + + static CCriticalSection cs_mapdstx; + + static void CheckDSTXes(int nHeight); + +public: + static void InitStandardDenominations(); + static std::vector GetStandardDenominations() { return vecStandardDenominations; } + static CAmount GetSmallestDenomination() { return vecStandardDenominations.back(); } + + //获取最大面额 + static int GetDenominationsByAmounts(const std::vector& vecAmount); + + static bool IsDenominatedAmount(CAmount nInputAmount); + + /// Get the denominations for a list of outputs (returns a bitshifted integer) + static int GetDenominations(const std::vector& vecTxOut, bool fSingleRandomDenom = false); + static std::string GetDenominationsToString(int nDenom); + static bool GetDenominationsBits(int nDenom, std::vector &vecBitsRet); + + static std::string GetMessageByID(PoolMessage nMessageID); + + /// 获取混币交易池的最大数量 + static int GetMaxPoolTransactions() { return Params().PoolMaxTransactions(); } + + static CAmount GetMaxPoolAmount() { return vecStandardDenominations.empty() ? 0 : PRIVATESEND_ENTRY_MAX_SIZE * vecStandardDenominations.front(); } + + //提到过的交易即抵押,这里判断其有效性 + static bool IsCollateralValid(const CTransaction& txCollateral); + static CAmount GetCollateralAmount() { return COLLATERAL; } + static CAmount GetMaxCollateralAmount() { return COLLATERAL*4; } + + //抵押的数量 + static bool IsCollateralAmount(CAmount nInputAmount); + //添加混币交易 + static void AddDSTX(const CDarksendBroadcastTx& dstx); + static CDarksendBroadcastTx GetDSTX(const uint256& hash); + + static void UpdatedBlockTip(const CBlockIndex *pindex); + static void SyncTransaction(const CTransaction& tx, const CBlock* pblock); +}; +``` +
+发现类中有大量的静态函数,这说明这个类被的相关接口可以直接访问而无需实例化。在chainparams.cpp中可以看到nPoolMaxTransactions=3,也就是前面提到的混币的交易是以3笔在一起的。 +
+下面分析一下中继交易,这种可以更好的隐藏交易的内容,下面是Client部分: + +```C++ +/** A mutable version of CTransaction. */ +struct CMutableTransaction +{ + int32_t nVersion; + std::vector vin; + std::vector vout; + uint32_t nLockTime; + + CMutableTransaction(); + CMutableTransaction(const CTransaction& tx); +...... + +}; +class CPrivateSendClient : public CPrivateSendBase +{ +private: + // Keep track of the used Masternodes + std::vector vecMasternodesUsed; + + std::vector vecDenominationsSkipped; + std::vector vecOutPointLocked; + + int nCachedLastSuccessBlock; + int nMinBlocksToWait; // how many blocks to wait after one successful mixing tx in non-multisession mode + + // Keep track of current block height + int nCachedBlockHeight; + + int nEntriesCount; + bool fLastEntryAccepted; + + std::string strLastMessage; + std::string strAutoDenomResult; + + masternode_info_t infoMixingMasternode; + //下面这个是前面提到的以抵押的方式提交的交易,这是数据结构 + CMutableTransaction txMyCollateral; // client side collateral + + CKeyHolderStorage keyHolderStorage; // storage for keys used in PrepareDenominate + + /// Check for process + void CheckPool(); + void CompletedTransaction(PoolMessage nMessageID); + + bool IsDenomSkipped(CAmount nDenomValue) { + return std::find(vecDenominationsSkipped.begin(), vecDenominationsSkipped.end(), nDenomValue) != vecDenominationsSkipped.end(); + } + + bool WaitForAnotherBlock(); + + // Make sure we have enough keys since last backup + bool CheckAutomaticBackup(); + bool JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman); + bool StartNewQueue(CAmount nValueMin, CAmount nBalanceNeedsAnonymized, CConnman& connman); + + /// Create denominations + bool CreateDenominated(CConnman& connman); + bool CreateDenominated(const CompactTallyItem& tallyItem, bool fCreateMixingCollaterals, CConnman& connman); + + /// Split up large inputs or make fee sized inputs + bool MakeCollateralAmounts(CConnman& connman); + bool MakeCollateralAmounts(const CompactTallyItem& tallyItem, bool fTryDenominated, CConnman& connman); + + //下面是分步发送交易的过程函数 + /// As a client, submit part of a future mixing transaction to a Masternode to start the process + bool SubmitDenominate(CConnman& connman); + /// step 1: prepare denominated inputs and outputs + bool PrepareDenominate(int nMinRounds, int nMaxRounds, std::string& strErrorRet, std::vector& vecTxDSInRet, std::vector& vecTxOutRet); + /// step 2: send denominated inputs and outputs prepared in step 1 + bool SendDenominate(const std::vector& vecTxDSIn, const std::vector& vecTxOut, CConnman& connman); + + /// Get Masternode updates about the progress of mixing + bool CheckPoolStateUpdate(PoolState nStateNew, int nEntriesCountNew, PoolStatusUpdate nStatusUpdate, PoolMessage nMessageID, int nSessionIDNew=0); + // Set the 'state' value, with some logging and capturing when the state changed + void SetState(PoolState nStateNew); + + /// As a client, check and sign the final transaction + bool SignFinalTransaction(const CTransaction& finalTransactionNew, CNode* pnode, CConnman& connman); + + void RelayIn(const CDarkSendEntry& entry, CConnman& connman); + + void SetNull(); + +public: + int nPrivateSendRounds; + int nPrivateSendAmount; + int nLiquidityProvider; + bool fEnablePrivateSend; + bool fPrivateSendMultiSession; + + int nCachedNumBlocks; //used for the overview screen + bool fCreateAutoBackups; //builtin support for automatic backups + + CPrivateSendClient() : + nCachedLastSuccessBlock(0), + nMinBlocksToWait(1), + txMyCollateral(CMutableTransaction()), + nPrivateSendRounds(DEFAULT_PRIVATESEND_ROUNDS), + nPrivateSendAmount(DEFAULT_PRIVATESEND_AMOUNT), + nLiquidityProvider(DEFAULT_PRIVATESEND_LIQUIDITY), + fEnablePrivateSend(false), + fPrivateSendMultiSession(DEFAULT_PRIVATESEND_MULTISESSION), + nCachedNumBlocks(std::numeric_limits::max()), + fCreateAutoBackups(true) { SetNull(); } + + void ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv, CConnman& connman); + + void ClearSkippedDenominations() { vecDenominationsSkipped.clear(); } + + void SetMinBlocksToWait(int nMinBlocksToWaitIn) { nMinBlocksToWait = nMinBlocksToWaitIn; } + + //在这个函数里可以看到对Pool的操作,其实主要还是操作上面的变量 + void ResetPool(); + + void UnlockCoins(); + + std::string GetStatus(); + + bool GetMixingMasternodeInfo(masternode_info_t& mnInfoRet); + bool IsMixingMasternode(const CNode* pnode); + + /// Passively run mixing in the background according to the configuration in settings + bool DoAutomaticDenominating(CConnman& connman, bool fDryRun=false); + + void CheckTimeout(); + + void UpdatedBlockTip(const CBlockIndex *pindex); +}; +``` +
+如果有比特币相关的知识会轻松不少,这里不展开分析具体的过程,只分析整体的流程。这里需要注意的是,一定是在masternode中进行交易的混合的,包括中继发送,都是一样的,这个一定不要弄混。 +
+下面看一下Server部分: + +```C++ +class CPrivateSendServer : public CPrivateSendBase +{ +private: + // Mixing uses collateral transactions to trust parties entering the pool + // to behave honestly. If they don't it takes their money. + std::vector vecSessionCollaterals; + + bool fUnitTest; + + /// Add a clients entry to the pool将交易添加到混币池 + bool AddEntry(const CDarkSendEntry& entryNew, PoolMessage& nMessageIDRet); + /// Add signature to a txin + bool AddScriptSig(const CTxIn& txin); + + /// Charge fees to bad actors (Charge clients a fee if they're abusive) + //在前面提到过交易的FEE是单独计算的 + void ChargeFees(CConnman& connman); + /// Rarely charge fees to pay miners + void ChargeRandomFees(CConnman& connman); + + /// Check for process + void CheckPool(CConnman& connman); + //创建和提交最终的交易 + void CreateFinalTransaction(CConnman& connman); + void CommitFinalTransaction(CConnman& connman); + + // 是否接受交易的抵押? + bool IsAcceptableDenomAndCollateral(int nDenom, CTransaction txCollateral, PoolMessage &nMessageIDRet); + //创建一个新的会话 + bool CreateNewSession(int nDenom, CTransaction txCollateral, PoolMessage &nMessageIDRet, CConnman& connman); + bool AddUserToExistingSession(int nDenom, CTransaction txCollateral, PoolMessage &nMessageIDRet); + /// Do we have enough users to take entries? + bool IsSessionReady() { return (int)vecSessionCollaterals.size() >= CPrivateSend::GetMaxPoolTransactions(); } + + /// Check that all inputs are signed. (Are all inputs signed?) + bool IsSignaturesComplete(); + /// Check to make sure a given input matches an input in the pool and its scriptSig is valid + bool IsInputScriptSigValid(const CTxIn& txin); + /// Are these outputs compatible with other client in the pool? + bool IsOutputsCompatibleWithSessionDenom(const std::vector& vecTxOut); + + // Set the 'state' value, with some logging and capturing when the state changed + void SetState(PoolState nStateNew); + + /// Relay mixing Messages + void RelayFinalTransaction(const CTransaction& txFinal, CConnman& connman); + void PushStatus(CNode* pnode, PoolStatusUpdate nStatusUpdate, PoolMessage nMessageID, CConnman& connman); + void RelayStatus(PoolStatusUpdate nStatusUpdate, CConnman& connman, PoolMessage nMessageID = MSG_NOERR); + void RelayCompletedTransaction(PoolMessage nMessageID, CConnman& connman); + + void SetNull(); + +public: + CPrivateSendServer() : + fUnitTest(false) { SetNull(); } + + void ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv, CConnman& connman); + + void CheckTimeout(CConnman& connman); + void CheckForCompleteQueue(CConnman& connman); +}; +``` +
+客户端和服务端的交互,互相传递相关的私有的发送交易和相关的签名,从而达到进一步的匿名的效果,这个确实是一个比较好的想法,应该是从工程上解决了算法上的一些遗憾。 +
+混币的细节这里就不详细阐述了,没有什么复杂之处。 + +## 五、总结 +通过上述的分析,可以看到,达世币在原有的混币的基础上,改进和完善了coinjoin的机制,使用得其混币技术更加完善。从理论上讲,追踪达世币的交易的路径比之简单的混币后的追踪,成本高出了很多,成本其实就决定了对匿名性的保护。 +
diff --git "a/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\272\214\351\227\250\347\275\227\345\270\201\347\232\204\347\216\257\347\255\276\345\220\215\345\210\206\346\236\220.md" "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\272\214\351\227\250\347\275\227\345\270\201\347\232\204\347\216\257\347\255\276\345\220\215\345\210\206\346\236\220.md" new file mode 100644 index 0000000..dbbdcec --- /dev/null +++ "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\344\272\214\351\227\250\347\275\227\345\270\201\347\232\204\347\216\257\347\255\276\345\220\215\345\210\206\346\236\220.md" @@ -0,0 +1,299 @@ +# 门罗币、ZEC和达世币的对比分析之二门罗的环签名分析 + +## 一、环签名的数学解析 +其实按照门罗币的加密机制,是分为环签名和环签名机密交易这两部分的,但是由于它们有很多的相关信息,所以它们的实现是写在了一起,这也导致了人们提及门罗币的匿名性就只提环签名。 +
+ +![mlsag1](img/MLSAG_01.png) +
+ +![mlsag2](img/MLSAG_02.png) +
+上面的算法是从门罗币的Ring Confidential Transactions中截取出来的。它主要有以下几个步骤来完成 : +
+1、首先是产生数据,也就是keygen的过程,注意G是一个ed25519的基础数据(基域),然后利用Hp这个Keccak(P)哈希函数来生成数据。 +
+2、生成随机值。其值域在Zq(ed25519的基础域) +
+3、计算L,R和c,其实就是不断的利用哈希函数和相关的公式进行计算和签名(sj = a-cj*xj),在这个计算的过程中,计算的输入和计算的输出形成一个环。 +
+4、得到最终的签名-σ(小写的西格马)。 +
+这其中有两个重要的公式: y^2 = x^3 + 486662^2 + x,这个椭圆曲线函数和SHA-512 +
+当验证者进行环签名验证时: +
+1、计算c = h(m,Li,Ri)。 +
+3、计算Ck,v(y1,y2,…yr),并验证其结果是否等于v。 +
+从上面的过程来看,其实大多数的算法过程都是成熟的算法,只是在签名过程中,使用数组 +
+在最新的门罗币中环签名使用了增强的Ring CT: +
+Ring CT 在链上隐藏了交易的数额即所有的交易输入都不需要再被拆分为已知的面额(早期的门罗币是要拆成固定的金额份数的),钱包现在可以从任意的 Ring CT 输出中选择 ring 成员。 +
+具体内容可以查看一下论文。https://eprint.iacr.org/2015/1098.pdf +
+更详细的相关资料请查看 +
+https://lab.getmonero.org/pubs/MRL-0004.pdf +https://lab.getmonero.org/pubs/MRL-0005.pdf + +## 二、门罗币中对环签名的使用流程 + +环签名的使用流程可以借用网上的两张图片来展示: +
+ +![MLSAG3](img/MLSAG_03.png) +
+“交易”分为两部分:交易前缀、交易后缀。 +
+“交易前缀”分为四部分:输入、额外数据、输出及交易元数据。 +
+“交易前缀”是除“签名”外的全部数据,即“交易后缀”包含的是“签名”相关数据。 +
+上一次交易的“额外数据”、“输出”会在下一次进行交易时用到。 +
+接收方仅需要提供自己的地址,用于生成一次性地址,其它步骤均不参与。 +
+“交易后缀”主要就是签名数据,包括原来的“环签”,和新的“环机密交易” +
+“额外数据”,除了 R 之外,还有“Transaction padding”和“ nonce”,这里没有表现出来,并且也不参与整个交易过程。 +
+1 之前的公开交易钥匙,即 R +
+2 之前的一次性公钥,即 P +
+3 发送的私钥,用于签名 +
+1、2、3 生成一次性公钥、私钥。私钥是 x,公钥是 P' +
+x 用于下面的钥匙镜像和环签。 +
+P' 和之前的 P 验证。 +
+12、13、14 参与环签。新的签名数据放到新的”交易后缀“。 +
+10 -> 13 包含了索引和金额数据。 +
+5 随机数 r 到 R +
+输入包含:发送金额和发送钥匙。都是处理过的 +
+输出包含:接收金额和接收钥匙。都是处理过的 +
+额外数据:交易公钥 R +
+交易后缀:就是签名数据 +
+ +![MLSAG4](img/MLSAG_04.png) +
+门罗中用linkability来避免双花。交易的输入对应一个一次性私钥,当持有一次性私钥的人花费这笔钱时,需要提供证明的签名,签名内容包含是密钥图像(key image),它的计算与私钥有关,所以当验证者发现两笔交易的密钥图像一致即可判断为双花交易。 +
+ +## 三、门罗币中环签名代码分析 +在门罗币的代码中,ringct的代码量并不很多,功能接口也比较直观: +
+首先创建一个Rct的签名 +
+ +``` C++ +rctSig genRct(const key &message, const ctkeyV & inSk, const keyV & destinations, const vector & amounts, const ctkeyM &mixRing, const keyV &amount_keys, const multisig_kLRki *kLRki, multisig_out *msout, unsigned int index, ctkeyV &outSk, bool bulletproof, hw::device &hwdev) { + CHECK_AND_ASSERT_THROW_MES(amounts.size() == destinations.size() || amounts.size() == destinations.size() + 1, "Different number of amounts/destinations"); + CHECK_AND_ASSERT_THROW_MES(amount_keys.size() == destinations.size(), "Different number of amount_keys/destinations"); + CHECK_AND_ASSERT_THROW_MES(index < mixRing.size(), "Bad index into mixRing"); + for (size_t n = 0; n < mixRing.size(); ++n) { + CHECK_AND_ASSERT_THROW_MES(mixRing[n].size() == inSk.size(), "Bad mixRing size"); + } + CHECK_AND_ASSERT_THROW_MES((kLRki && msout) || (!kLRki && !msout), "Only one of kLRki/msout is present"); + + //输入参数的匹配 + rctSig rv; + rv.type = bulletproof ? RCTTypeFullBulletproof : RCTTypeFull; + rv.message = message; + rv.outPk.resize(destinations.size()); + if (bulletproof) + rv.p.bulletproofs.resize(destinations.size()); + else + rv.p.rangeSigs.resize(destinations.size()); + rv.ecdhInfo.resize(destinations.size()); + + size_t i = 0; + keyV masks(destinations.size()); //sk mask.. + outSk.resize(destinations.size()); + for (i = 0; i < destinations.size(); i++) { + //add destination to sig + rv.outPk[i].dest = copy(destinations[i]); + //数据的验证 + //compute range proof + if (bulletproof) + rv.p.bulletproofs[i] = proveRangeBulletproof(rv.outPk[i].mask, outSk[i].mask, amounts[i]); + else + rv.p.rangeSigs[i] = proveRange(rv.outPk[i].mask, outSk[i].mask, amounts[i]); + #ifdef DBG + if (bulletproof) + CHECK_AND_ASSERT_THROW_MES(verBulletproof(rv.p.bulletproofs[i]), "verBulletproof failed on newly created proof"); + else + CHECK_AND_ASSERT_THROW_MES(verRange(rv.outPk[i].mask, rv.p.rangeSigs[i]), "verRange failed on newly created proof"); + #endif + + //mask amount and mask + rv.ecdhInfo[i].mask = copy(outSk[i].mask); + rv.ecdhInfo[i].amount = d2h(amounts[i]); + hwdev.ecdhEncode(rv.ecdhInfo[i], amount_keys[i]); + } + + //set txn fee + if (amounts.size() > destinations.size()) + { + rv.txnFee = amounts[destinations.size()]; + } + else + { + rv.txnFee = 0; + } + key txnFeeKey = scalarmultH(d2h(rv.txnFee)); + + rv.mixRing = mixRing; + if (msout) + msout->c.resize(1); + rv.p.MGs.push_back(proveRctMG(get_pre_mlsag_hash(rv, hwdev), rv.mixRing, inSk, outSk, rv.outPk, kLRki, msout ? &msout->c[0] : NULL, index, txnFeeKey,hwdev)); + return rv; +} + +``` +
+这段代码产生一个相关的签名,其中必须包含相关rangeProofs验证需要的数据。签名者相关的数据必须在输入数据中。而且要保证输入集数据之和与输出集数据之和相等。 +
+真正的处理在: +
+ +``` C++ +mgSig proveRctMG(const key &message, const ctkeyM & pubs, const ctkeyV & inSk, const ctkeyV &outSk, const ctkeyV & outPk, const multisig_kLRki *kLRki, key *mscout, unsigned int index, key txnFeeKey, hw::device &hwdev) { + mgSig mg; + //setup vars + size_t cols = pubs.size(); + CHECK_AND_ASSERT_THROW_MES(cols >= 1, "Empty pubs"); + size_t rows = pubs[0].size(); + CHECK_AND_ASSERT_THROW_MES(rows >= 1, "Empty pubs"); + for (size_t i = 1; i < cols; ++i) { + CHECK_AND_ASSERT_THROW_MES(pubs[i].size() == rows, "pubs is not rectangular"); + } + CHECK_AND_ASSERT_THROW_MES(inSk.size() == rows, "Bad inSk size"); + CHECK_AND_ASSERT_THROW_MES(outSk.size() == outPk.size(), "Bad outSk/outPk size"); + CHECK_AND_ASSERT_THROW_MES((kLRki && mscout) || (!kLRki && !mscout), "Only one of kLRki/mscout is present"); + + keyV sk(rows + 1); + keyV tmp(rows + 1); + size_t i = 0, j = 0; + for (i = 0; i < rows + 1; i++) { + sc_0(sk[i].bytes); + identity(tmp[i]); + } + keyM M(cols, tmp); + //create the matrix to mg sig + for (i = 0; i < cols; i++) { + M[i][rows] = identity(); + for (j = 0; j < rows; j++) { + M[i][j] = pubs[i][j].dest; + addKeys(M[i][rows], M[i][rows], pubs[i][j].mask); //add input commitments in last row + } + } + sc_0(sk[rows].bytes); + for (j = 0; j < rows; j++) { + sk[j] = copy(inSk[j].dest); + sc_add(sk[rows].bytes, sk[rows].bytes, inSk[j].mask.bytes); //add masks in last row + } + for (i = 0; i < cols; i++) { + for (size_t j = 0; j < outPk.size(); j++) { + subKeys(M[i][rows], M[i][rows], outPk[j].mask); //subtract output Ci's in last row + } + //subtract txn fee output in last row + subKeys(M[i][rows], M[i][rows], txnFeeKey); + } + for (size_t j = 0; j < outPk.size(); j++) { + sc_sub(sk[rows].bytes, sk[rows].bytes, outSk[j].mask.bytes); //subtract output masks in last row.. + } + return MLSAG_Gen(message, M, sk, kLRki, mscout, index, rows, hwdev); +} +``` +
+这段代码是环签名的重要环节,它通过对KEY的反复处理,达到最终的求σ。这里的细节就不展开了。 +
+如果需要对签名进行验证使用下面的代码: +
+ +``` C++ + +bool verRct(const rctSig & rv, bool semantics) { + PERF_TIMER(verRct); + CHECK_AND_ASSERT_MES(rv.type == RCTTypeFull || rv.type == RCTTypeFullBulletproof, false, "verRct called on non-full rctSig"); + if (semantics) + { + if (rv.type == RCTTypeFullBulletproof) + CHECK_AND_ASSERT_MES(rv.outPk.size() == rv.p.bulletproofs.size(), false, "Mismatched sizes of outPk and rv.p.bulletproofs"); + else + CHECK_AND_ASSERT_MES(rv.outPk.size() == rv.p.rangeSigs.size(), false, "Mismatched sizes of outPk and rv.p.rangeSigs"); + CHECK_AND_ASSERT_MES(rv.outPk.size() == rv.ecdhInfo.size(), false, "Mismatched sizes of outPk and rv.ecdhInfo"); + CHECK_AND_ASSERT_MES(rv.p.MGs.size() == 1, false, "full rctSig has not one MG"); + } + else + { + // semantics check is early, we don't have the MGs resolved yet + } + + // some rct ops can throw + try + { + if (semantics) { + tools::threadpool& tpool = tools::threadpool::getInstance(); + tools::threadpool::waiter waiter; + std::deque results(rv.outPk.size(), false); + DP("range proofs verified?"); + for (size_t i = 0; i < rv.outPk.size(); i++) { + tpool.submit(&waiter, [&, i] { + if (rv.p.rangeSigs.empty()) + results[i] = verBulletproof(rv.p.bulletproofs[i]); + else + results[i] = verRange(rv.outPk[i].mask, rv.p.rangeSigs[i]); + }); + } + waiter.wait(); + + for (size_t i = 0; i < rv.outPk.size(); ++i) { + if (!results[i]) { + LOG_PRINT_L1("Range proof verified failed for output " << i); + return false; + } + } + } + + if (!semantics) { + //compute txn fee + key txnFeeKey = scalarmultH(d2h(rv.txnFee)); + bool mgVerd = verRctMG(rv.p.MGs[0], rv.mixRing, rv.outPk, txnFeeKey, get_pre_mlsag_hash(rv, hw::get_device("default"))); + DP("mg sig verified?"); + DP(mgVerd); + if (!mgVerd) { + LOG_PRINT_L1("MG signature verification failed"); + return false; + } + } + + return true; + } +...... +} + +``` + +
+从源码中可以看到,RCT中使用的相关的加密的很多相关算法都是使用用的标准的加密的方法,这也说明RCT只是对原有环签名的一个改进而不是革命。在验证的接口中,主要是对前面提到的签名完成后的验证环节的实现。 + +## 四、总结 +门罗币的环机密交易签名机制虽然在匿名交易中有着快速安全的特点,但是参与者的选取等问题,仍然存在着可能泄露交易信息的情况。但是相对于比特币和以太坊,门罗币则要安全许多,这也导致很多的灰色交易转到了以门罗币的支付上来。 +
+其实分析下来发现,数学还是基础,没有数学的证明,都是浮云。 +
diff --git "a/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\345\233\233Zcash\347\232\204\351\233\266\347\237\245\350\257\206\350\257\201\346\230\216\345\210\206\346\236\220.md" "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\345\233\233Zcash\347\232\204\351\233\266\347\237\245\350\257\206\350\257\201\346\230\216\345\210\206\346\236\220.md" new file mode 100644 index 0000000..131d244 --- /dev/null +++ "b/privacy/\351\227\250\347\275\227\345\270\201\343\200\201ZEC\345\222\214\350\276\276\344\270\226\345\270\201\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\344\271\213\345\233\233Zcash\347\232\204\351\233\266\347\237\245\350\257\206\350\257\201\346\230\216\345\210\206\346\236\220.md" @@ -0,0 +1,859 @@ +# 门罗币、ZEC和达世币的对比分析之四Zcash的零知识证明分析 + +## 一、Zcash特点 +Zcah的主要源码是从比特币转过来的。所以说可以把它看成比特币的的一个分支。这里需要说明的是,一定要把Zcash和Zcoin区别开来。两者都使用了零知识证明。只不过二者使用的零知识证明的算法不同,Zcash使用的是libsnark的相关算法。 +
+相对于其它匿名方式,使用零知识证明的方式可以说是最优的。但是零知识证明的交互式证明的方式,却在无形间增加了整个交易处理的时间。同时,使用的信任机制中,还是存在着恶意串通的可能。不过Zcash中使用的零知识证明算法是不需要交互的,不过多轮的验证仍然使它耗费的时间很多(30~40秒,不过具说目前减化版已经缩小到了7秒)。 +
+相对于其它算法,zkSNARKs算法的应用时间还是比较短,尚需要进一步的验证其它安全性和合理性。 + +## 二、零知识证明的原理 + +目前的零知识证明主要有两大类,分为Bulletproofs和rangeproofs。而Zcash使用的是后者。前者据说比后者要更快更安全,但仍然没有生产性的环节进行验证,也就是说,所以有工作仍然是纸面上的。 +
+零知识证明的数学方式其实就是NP的问题。也就是说,把一个问题通过某种手段可以转换成一个多项式时间内的验证的问题(求解是P)。而Zcash使用的算法适合于QAP问题(不必计较,这个其实是数学上经典的二次分配问题)。 +
+Goldreich等人已经证明了任何NP成员问题都有零知识证明系统,因此其在密码学中的应用日益广泛。下面简述一下零知识证明的数学赛程: +
+1、QAP问题的转化。 +
+2、NP问题的转化。 +
+3、利用拉格朗日插值法进行多项式的处理。 +
+4、抽样点的数学验证。 +
+5、抽样点的同态映射保护。 +
+6、KCA(Knowledge of Coefficient Test and Assumption):强制按约定验证。 +
+在同态映射保护中,目前更好的方法还有双线性映射和椭圆曲线的方式。不过,方法越好,可能复杂度就越高。到实践应用还需要更好的进行算法的优化。 +
+更具体的说明请查看相关的算法: +SCIPR实验室的libsnark:https://github.com/scipr-lab/libsnark。 +
+Zcash修改后的libsnark:https://github.com/zcash/libsnark。 + +## 三、Zcash中对零知识证明的应用 +zk-SNARK是“zero knowledge Succinct Non-interactive ARgument of Knowledge”的缩写,它有三个显著的特点: +
+1、零知识:证明过程不透露任何消息。 +
+2、简洁:不涉及大量数据传输和验证。 +
+3、无交互:不像网上的阿里巴巴等的例子,需要不断的进行交互问答。 +
+这里只说明匿名交易,对透明交易不处理。 +
+在Zcashk,一个交易的输入和输出都是若干note。为描述方便起见,将note记为“note=(PK, v, r)”,其中,PK是所有者的公钥(地址),v是金额,而r是可以唯一区分该note的序列号。输入、输出不再是明文的note,而分别是note的废止通知和签发通知: +
+签发通知(note commitment):作为交易的输出,表示一张新note被签发。一个有效的commitment是一张note存在的证明,然而从它包含的信息中并不知道是哪张note,也就无法知道所有者是谁,金额多少。为满足这一点,最简单的方法是对note的描述信息取哈希,因此note对应的commitment可以简单描述为“HASH(note)”。 +
+废止通知(note nullifier):作为交易的输入,表示一张老支票将作废(因为马上要被兑现、花掉了)。同比特币一样,一个交易的输入一定是另一个交易的输出,因此nullifier对应唯一一个commitment(结合commitment的定义,也就唯一对应一张note),但从它包含的信息并不能推导出是哪个commitment(如果可以的话,ZCash交易便可被追踪,因而丧失隐私性了)。为构造满足要求的nullifier,取哈希依然是个好办法,因此序号为r的note,对应的nullifier可描述为“HASH(r)”。 +
+通过引入nullifier和commitment,交易之间的种种表现出来的实际的内容,变成了一种互相的心知肚明。区块链的共识者各自维护一个nullifer和commitment的集合,这个集合会根据交易的不同时间进行动态的变化。。 +
+当广播消息来到,节点会通过二者进行验证和处理,同时使用零知识证明来控制权限的使用(也即支付或者兑现的权限)。 +
+ +## 四、源码分析 +在Zcash中,使用了libsnark的库,所以,零知识证明是围绕着库的调用展开的。这里分析一下这个库的源码,在原来的标准的零知识证明库中有两个版本的生成,这里只使用了gadgetbli1.先看一下初步的上层调用: +
+ +```C++ +template +class joinsplit_gadget : gadget { +private: + // Verifier inputs + pb_variable_array zk_packed_inputs; + pb_variable_array zk_unpacked_inputs; + std::shared_ptr> unpacker; + + std::shared_ptr> zk_merkle_root; + std::shared_ptr> zk_h_sig; + boost::array>, NumInputs> zk_input_nullifiers; + boost::array>, NumInputs> zk_input_macs; + boost::array>, NumOutputs> zk_output_commitments; + pb_variable_array zk_vpub_old; + pb_variable_array zk_vpub_new; + + // Aux inputs + pb_variable ZERO; + std::shared_ptr> zk_phi; + pb_variable_array zk_total_uint64; + + // Input note gadgets + boost::array>, NumInputs> zk_input_notes; + boost::array>, NumInputs> zk_mac_authentication; + + // Output note gadgets + boost::array>, NumOutputs> zk_output_notes; + +public: + // PRF_pk only has a 1-bit domain separation "nonce" + // for different macs. + BOOST_STATIC_ASSERT(NumInputs <= 2); + + // PRF_rho only has a 1-bit domain separation "nonce" + // for different output `rho`. + BOOST_STATIC_ASSERT(NumOutputs <= 2); + + joinsplit_gadget(protoboard &pb) : gadget(pb) { + // Verification + { + // The verification inputs are all bit-strings of various + // lengths (256-bit digests and 64-bit integers) and so we + // pack them into as few field elements as possible. (The + // more verification inputs you have, the more expensive + // verification is.) + zk_packed_inputs.allocate(pb, verifying_field_element_size()); + pb.set_input_sizes(verifying_field_element_size()); + + alloc_uint256(zk_unpacked_inputs, zk_merkle_root); + alloc_uint256(zk_unpacked_inputs, zk_h_sig); + + for (size_t i = 0; i < NumInputs; i++) { + alloc_uint256(zk_unpacked_inputs, zk_input_nullifiers[i]); + alloc_uint256(zk_unpacked_inputs, zk_input_macs[i]); + } + + for (size_t i = 0; i < NumOutputs; i++) { + alloc_uint256(zk_unpacked_inputs, zk_output_commitments[i]); + } + + alloc_uint64(zk_unpacked_inputs, zk_vpub_old); + alloc_uint64(zk_unpacked_inputs, zk_vpub_new); + + assert(zk_unpacked_inputs.size() == verifying_input_bit_size()); + + // This gadget will ensure that all of the inputs we provide are + // boolean constrained. + unpacker.reset(new multipacking_gadget( + pb, + zk_unpacked_inputs, + zk_packed_inputs, + FieldT::capacity(), + "unpacker" + )); + } + + // We need a constant "zero" variable in some contexts. In theory + // it should never be necessary, but libsnark does not synthesize + // optimal circuits. + // + // The first variable of our constraint system is constrained + // to be one automatically for us, and is known as `ONE`. + ZERO.allocate(pb); + + zk_phi.reset(new digest_variable(pb, 252, "")); + + zk_total_uint64.allocate(pb, 64); + + for (size_t i = 0; i < NumInputs; i++) { + // Input note gadget for commitments, macs, nullifiers, + // and spend authority. + zk_input_notes[i].reset(new input_note_gadget( + pb, + ZERO, + zk_input_nullifiers[i], + *zk_merkle_root + )); + + // The input keys authenticate h_sig to prevent + // malleability. + zk_mac_authentication[i].reset(new PRF_pk_gadget( + pb, + ZERO, + zk_input_notes[i]->a_sk->bits, + zk_h_sig->bits, + i ? true : false, + zk_input_macs[i] + )); + } + + for (size_t i = 0; i < NumOutputs; i++) { + zk_output_notes[i].reset(new output_note_gadget( + pb, + ZERO, + zk_phi->bits, + zk_h_sig->bits, + i ? true : false, + zk_output_commitments[i] + )); + } + } + + void generate_r1cs_constraints() { + // The true passed here ensures all the inputs + // are boolean constrained. + unpacker->generate_r1cs_constraints(true); + + // Constrain `ZERO` + generate_r1cs_equals_const_constraint(this->pb, ZERO, FieldT::zero(), "ZERO"); + + // Constrain bitness of phi + zk_phi->generate_r1cs_constraints(); + + for (size_t i = 0; i < NumInputs; i++) { + // Constrain the JoinSplit input constraints. + zk_input_notes[i]->generate_r1cs_constraints(); + + // Authenticate h_sig with a_sk + zk_mac_authentication[i]->generate_r1cs_constraints(); + } + + for (size_t i = 0; i < NumOutputs; i++) { + // Constrain the JoinSplit output constraints. + zk_output_notes[i]->generate_r1cs_constraints(); + } + + // Value balance + { + linear_combination left_side = packed_addition(zk_vpub_old); + for (size_t i = 0; i < NumInputs; i++) { + left_side = left_side + packed_addition(zk_input_notes[i]->value); + } + + linear_combination right_side = packed_addition(zk_vpub_new); + for (size_t i = 0; i < NumOutputs; i++) { + right_side = right_side + packed_addition(zk_output_notes[i]->value); + } + + // Ensure that both sides are equal + this->pb.add_r1cs_constraint(r1cs_constraint( + 1, + left_side, + right_side + )); + + // #854: Ensure that left_side is a 64-bit integer. + for (size_t i = 0; i < 64; i++) { + generate_boolean_r1cs_constraint( + this->pb, + zk_total_uint64[i], + "" + ); + } + + this->pb.add_r1cs_constraint(r1cs_constraint( + 1, + left_side, + packed_addition(zk_total_uint64) + )); + } + } + + void generate_r1cs_witness( + const uint252& phi, + const uint256& rt, + const uint256& h_sig, + const boost::array& inputs, + const boost::array& outputs, + uint64_t vpub_old, + uint64_t vpub_new + ) { + // Witness `zero` + this->pb.val(ZERO) = FieldT::zero(); + + // Witness rt. This is not a sanity check. + // + // This ensures the read gadget constrains + // the intended root in the event that + // both inputs are zero-valued. + zk_merkle_root->bits.fill_with_bits( + this->pb, + uint256_to_bool_vector(rt) + ); + + // Witness public balance values + zk_vpub_old.fill_with_bits( + this->pb, + uint64_to_bool_vector(vpub_old) + ); + zk_vpub_new.fill_with_bits( + this->pb, + uint64_to_bool_vector(vpub_new) + ); + + { + // Witness total_uint64 bits + uint64_t left_side_acc = vpub_old; + for (size_t i = 0; i < NumInputs; i++) { + left_side_acc += inputs[i].note.value(); + } + + zk_total_uint64.fill_with_bits( + this->pb, + uint64_to_bool_vector(left_side_acc) + ); + } + + // Witness phi + zk_phi->bits.fill_with_bits( + this->pb, + uint252_to_bool_vector(phi) + ); + + // Witness h_sig + zk_h_sig->bits.fill_with_bits( + this->pb, + uint256_to_bool_vector(h_sig) + ); + + for (size_t i = 0; i < NumInputs; i++) { + // Witness the input information. + auto merkle_path = inputs[i].witness.path(); + zk_input_notes[i]->generate_r1cs_witness( + merkle_path, + inputs[i].key, + inputs[i].note + ); + + // Witness macs + zk_mac_authentication[i]->generate_r1cs_witness(); + } + + for (size_t i = 0; i < NumOutputs; i++) { + // Witness the output information. + zk_output_notes[i]->generate_r1cs_witness(outputs[i]); + } + + // [SANITY CHECK] Ensure that the intended root + // was witnessed by the inputs, even if the read + // gadget overwrote it. This allows the prover to + // fail instead of the verifier, in the event that + // the roots of the inputs do not match the + // treestate provided to the proving API. + zk_merkle_root->bits.fill_with_bits( + this->pb, + uint256_to_bool_vector(rt) + ); + + // This happens last, because only by now are all the + // verifier inputs resolved. + unpacker->generate_r1cs_witness_from_bits(); + } + +``` +
+通过名字来看,它和库内部的函数名字操持一致,这几个函数主要用来提供处理交易前的数据,深入到库内部看一下相关的函数: +
+ +```C++ +template +void generate_boolean_r1cs_constraint(protoboard &pb, const pb_linear_combination &lc, const std::string &annotation_prefix) +/* forces lc to take value 0 or 1 by adding constraint lc * (1-lc) = 0 */ +{ + pb.add_r1cs_constraint(r1cs_constraint(lc, 1-lc, 0), + FMT(annotation_prefix, " boolean_r1cs_constraint")); +} + +template +void generate_r1cs_equals_const_constraint(protoboard &pb, const pb_linear_combination &lc, const FieldT& c, const std::string &annotation_prefix) +{ + pb.add_r1cs_constraint(r1cs_constraint(1, lc, c), + FMT(annotation_prefix, " constness_constraint")); +} + +template +void packing_gadget::generate_r1cs_constraints(const bool enforce_bitness) +/* adds constraint result = \sum bits[i] * 2^i */ +{ + this->pb.add_r1cs_constraint(r1cs_constraint(1, pb_packing_sum(bits), packed), FMT(this->annotation_prefix, " packing_constraint")); + + if (enforce_bitness) + { + for (size_t i = 0; i < bits.size(); ++i) + { + generate_boolean_r1cs_constraint(this->pb, bits[i], FMT(this->annotation_prefix, " bitness_%zu", i)); + } + } +} + +template +void field_vector_copy_gadget::generate_r1cs_witness() +{ + do_copy.evaluate(this->pb); + assert(this->pb.lc_val(do_copy) == FieldT::one() || this->pb.lc_val(do_copy) == FieldT::zero()); + if (this->pb.lc_val(do_copy) != FieldT::zero()) + { + for (size_t i = 0; i < source.size(); ++i) + { + this->pb.val(target[i]) = this->pb.val(source[i]); + } + } +} +``` +
+库中针对不同的情况展开了很多的重载函数,这里就不一一的列出了。然后需要进行QAP和NP的处理: +
+ +```C++ +template +qap_instance r1cs_to_qap_instance_map(const r1cs_constraint_system &cs) +{ + enter_block("Call to r1cs_to_qap_instance_map"); + + const std::shared_ptr > domain = get_evaluation_domain(cs.num_constraints() + cs.num_inputs() + 1); + + std::vector > A_in_Lagrange_basis(cs.num_variables()+1); + std::vector > B_in_Lagrange_basis(cs.num_variables()+1); + std::vector > C_in_Lagrange_basis(cs.num_variables()+1); + + enter_block("Compute polynomials A, B, C in Lagrange basis"); + /** + * add and process the constraints + * input_i * 0 = 0 + * to ensure soundness of input consistency + \*/ + for (size_t i = 0; i <= cs.num_inputs(); ++i) + { + A_in_Lagrange_basis[i][cs.num_constraints() + i] = FieldT::one(); + } + /* process all other constraints \*/ + for (size_t i = 0; i < cs.num_constraints(); ++i) + { + for (size_t j = 0; j < cs.constraints[i].a.terms.size(); ++j) + { + A_in_Lagrange_basis[cs.constraints[i].a.terms[j].index][i] += + cs.constraints[i].a.terms[j].coeff; + } + + for (size_t j = 0; j < cs.constraints[i].b.terms.size(); ++j) + { + B_in_Lagrange_basis[cs.constraints[i].b.terms[j].index][i] += + cs.constraints[i].b.terms[j].coeff; + } + + for (size_t j = 0; j < cs.constraints[i].c.terms.size(); ++j) + { + C_in_Lagrange_basis[cs.constraints[i].c.terms[j].index][i] += + cs.constraints[i].c.terms[j].coeff; + } + } + leave_block("Compute polynomials A, B, C in Lagrange basis"); + + leave_block("Call to r1cs_to_qap_instance_map"); + + return qap_instance(domain, + cs.num_variables(), + domain->m, + cs.num_inputs(), + std::move(A_in_Lagrange_basis), + std::move(B_in_Lagrange_basis), + std::move(C_in_Lagrange_basis)); +} + +template +size_t r1cs_constraint_system::num_inputs() const +{ + return primary_input_size; +} + +template +size_t r1cs_constraint_system::num_variables() const +{ + return primary_input_size + auxiliary_input_size; +} + + +template +size_t r1cs_constraint_system::num_constraints() const +{ + return constraints.size(); +} + +template +bool r1cs_constraint_system::is_valid() const +{ + if (this->num_inputs() > this->num_variables()) return false; + + for (size_t c = 0; c < constraints.size(); ++c) + { + if (!(constraints[c].a.is_valid(this->num_variables()) && + constraints[c].b.is_valid(this->num_variables()) && + constraints[c].c.is_valid(this->num_variables()))) + { + return false; + } + } + + return true; +} + +template +void dump_r1cs_constraint(const r1cs_constraint &constraint, + const r1cs_variable_assignment &full_variable_assignment, + const std::map &variable_annotations) +{ + printf("terms for a:\n"); constraint.a.print_with_assignment(full_variable_assignment, variable_annotations); + printf("terms for b:\n"); constraint.b.print_with_assignment(full_variable_assignment, variable_annotations); + printf("terms for c:\n"); constraint.c.print_with_assignment(full_variable_assignment, variable_annotations); +} +``` +
+这两个问题的主要代码在relations路径目录下,有QAP和R1CS两个部分。这个涉及到比较复杂的数学知识,这里不展开了,别误导大家。 +
+验证接口在zk_proof_systems目录下,它会调用相关的验证函数进行对上面的多项式的验证。 +
+ +```C++ +template +bool r1cs_ppzksnark_verifier_weak_IC(const r1cs_ppzksnark_verification_key &vk, + const r1cs_ppzksnark_primary_input &primary_input, + const r1cs_ppzksnark_proof &proof) +{ + enter_block("Call to r1cs_ppzksnark_verifier_weak_IC"); + r1cs_ppzksnark_processed_verification_key pvk = r1cs_ppzksnark_verifier_process_vk(vk); + bool result = r1cs_ppzksnark_online_verifier_weak_IC(pvk, primary_input, proof); + leave_block("Call to r1cs_ppzksnark_verifier_weak_IC"); + return result; +} + +template +bool r1cs_ppzksnark_online_verifier_strong_IC(const r1cs_ppzksnark_processed_verification_key &pvk, + const r1cs_ppzksnark_primary_input &primary_input, + const r1cs_ppzksnark_proof &proof) +{ + bool result = true; + enter_block("Call to r1cs_ppzksnark_online_verifier_strong_IC"); + + if (pvk.encoded_IC_query.domain_size() != primary_input.size()) + { + print_indent(); printf("Input length differs from expected (got %zu, expected %zu).\n", primary_input.size(), pvk.encoded_IC_query.domain_size()); + result = false; + } + else + { + result = r1cs_ppzksnark_online_verifier_weak_IC(pvk, primary_input, proof); + } + + leave_block("Call to r1cs_ppzksnark_online_verifier_strong_IC"); + return result; +} + +template +bool r1cs_ppzksnark_verifier_strong_IC(const r1cs_ppzksnark_verification_key &vk, + const r1cs_ppzksnark_primary_input &primary_input, + const r1cs_ppzksnark_proof &proof) +{ + enter_block("Call to r1cs_ppzksnark_verifier_strong_IC"); + r1cs_ppzksnark_processed_verification_key pvk = r1cs_ppzksnark_verifier_process_vk(vk); + bool result = r1cs_ppzksnark_online_verifier_strong_IC(pvk, primary_input, proof); + leave_block("Call to r1cs_ppzksnark_verifier_strong_IC"); + return result; +} +``` +
+在这其中,还有相关的代码,可以去看源码。 +
+ +```C++ +template +class JoinSplitCircuit : public JoinSplit { +public: + typedef default_r1cs_ppzksnark_pp ppzksnark_ppT; + typedef Fr FieldT; + + r1cs_ppzksnark_verification_key vk; + r1cs_ppzksnark_processed_verification_key vk_precomp; + std::string pkPath; + + JoinSplitCircuit(const std::string vkPath, const std::string pkPath) : pkPath(pkPath) { + loadFromFile(vkPath, vk); + vk_precomp = r1cs_ppzksnark_verifier_process_vk(vk); + } + ~JoinSplitCircuit() {} + + static void generate(const std::string r1csPath, + const std::string vkPath, + const std::string pkPath) + { + protoboard pb; + + joinsplit_gadget g(pb); + g.generate_r1cs_constraints(); + + auto r1cs = pb.get_constraint_system(); + + saveToFile(r1csPath, r1cs); + + r1cs_ppzksnark_keypair keypair = r1cs_ppzksnark_generator(r1cs); + + saveToFile(vkPath, keypair.vk); + saveToFile(pkPath, keypair.pk); + } + + bool verify( + const ZCProof& proof, + ProofVerifier& verifier, + const uint256& pubKeyHash, + const uint256& randomSeed, + const boost::array& macs, + const boost::array& nullifiers, + const boost::array& commitments, + uint64_t vpub_old, + uint64_t vpub_new, + const uint256& rt + ) { + try { + auto r1cs_proof = proof.to_libsnark_proof>(); + + uint256 h_sig = this->h_sig(randomSeed, nullifiers, pubKeyHash); + + auto witness = joinsplit_gadget::witness_map( + rt, + h_sig, + macs, + nullifiers, + commitments, + vpub_old, + vpub_new + ); + + return verifier.check( + vk, + vk_precomp, + witness, + r1cs_proof + ); + } catch (...) { + return false; + } + } + + SproutProof prove( + bool makeGrothProof, + const boost::array& inputs, + const boost::array& outputs, + boost::array& out_notes, + boost::array& out_ciphertexts, + uint256& out_ephemeralKey, + const uint256& pubKeyHash, + uint256& out_randomSeed, + boost::array& out_macs, + boost::array& out_nullifiers, + boost::array& out_commitments, + uint64_t vpub_old, + uint64_t vpub_new, + const uint256& rt, + bool computeProof, + uint256 *out_esk // Payment disclosure + ) { + if (vpub_old > MAX_MONEY) { + throw std::invalid_argument("nonsensical vpub_old value"); + } + + if (vpub_new > MAX_MONEY) { + throw std::invalid_argument("nonsensical vpub_new value"); + } + + uint64_t lhs_value = vpub_old; + uint64_t rhs_value = vpub_new; + + for (size_t i = 0; i < NumInputs; i++) { + // Sanity checks of input + { + // If note has nonzero value + if (inputs[i].note.value() != 0) { + // The witness root must equal the input root. + if (inputs[i].witness.root() != rt) { + throw std::invalid_argument("joinsplit not anchored to the correct root"); + } + + // The tree must witness the correct element + if (inputs[i].note.cm() != inputs[i].witness.element()) { + throw std::invalid_argument("witness of wrong element for joinsplit input"); + } + } + + // Ensure we have the key to this note. + if (inputs[i].note.a_pk != inputs[i].key.address().a_pk) { + throw std::invalid_argument("input note not authorized to spend with given key"); + } + + // Balance must be sensical + if (inputs[i].note.value() > MAX_MONEY) { + throw std::invalid_argument("nonsensical input note value"); + } + + lhs_value += inputs[i].note.value(); + + if (lhs_value > MAX_MONEY) { + throw std::invalid_argument("nonsensical left hand size of joinsplit balance"); + } + } + + // Compute nullifier of input + out_nullifiers[i] = inputs[i].nullifier(); + } + + // Sample randomSeed + out_randomSeed = random_uint256(); + + // Compute h_sig + uint256 h_sig = this->h_sig(out_randomSeed, out_nullifiers, pubKeyHash); + + // Sample phi + uint252 phi = random_uint252(); + + // Compute notes for outputs + for (size_t i = 0; i < NumOutputs; i++) { + // Sanity checks of output + { + if (outputs[i].value > MAX_MONEY) { + throw std::invalid_argument("nonsensical output value"); + } + + rhs_value += outputs[i].value; + + if (rhs_value > MAX_MONEY) { + throw std::invalid_argument("nonsensical right hand side of joinsplit balance"); + } + } + + // Sample r + uint256 r = random_uint256(); + + out_notes[i] = outputs[i].note(phi, r, i, h_sig); + } + + if (lhs_value != rhs_value) { + throw std::invalid_argument("invalid joinsplit balance"); + } + + // Compute the output commitments + for (size_t i = 0; i < NumOutputs; i++) { + out_commitments[i] = out_notes[i].cm(); + } + + // Encrypt the ciphertexts containing the note + // plaintexts to the recipients of the value. + { + ZCNoteEncryption encryptor(h_sig); + + for (size_t i = 0; i < NumOutputs; i++) { + SproutNotePlaintext pt(out_notes[i], outputs[i].memo); + + out_ciphertexts[i] = pt.encrypt(encryptor, outputs[i].addr.pk_enc); + } + + out_ephemeralKey = encryptor.get_epk(); + + // !!! Payment disclosure START + if (out_esk != nullptr) { + \*out_esk = encryptor.get_esk(); + } + // !!! Payment disclosure END + } + + // Authenticate h_sig with each of the input + // spending keys, producing macs which protect + // against malleability. + for (size_t i = 0; i < NumInputs; i++) { + out_macs[i] = PRF_pk(inputs[i].key, i, h_sig); + } + + if (makeGrothProof) { + if (!computeProof) { + return GrothProof(); + } + + GrothProof proof; + + CDataStream ss1(SER_NETWORK, PROTOCOL_VERSION); + ss1 << inputs[0].witness.path(); + std::vector auth1(ss1.begin(), ss1.end()); + + CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION); + ss2 << inputs[1].witness.path(); + std::vector auth2(ss2.begin(), ss2.end()); + + librustzcash_sprout_prove( + proof.begin(), + + phi.begin(), + rt.begin(), + h_sig.begin(), + + inputs[0].key.begin(), + inputs[0].note.value(), + inputs[0].note.rho.begin(), + inputs[0].note.r.begin(), + auth1.data(), + + inputs[1].key.begin(), + inputs[1].note.value(), + inputs[1].note.rho.begin(), + inputs[1].note.r.begin(), + auth2.data(), + + out_notes[0].a_pk.begin(), + out_notes[0].value(), + out_notes[0].r.begin(), + + out_notes[1].a_pk.begin(), + out_notes[1].value(), + out_notes[1].r.begin(), + + vpub_old, + vpub_new + ); + + return proof; + } + + if (!computeProof) { + return ZCProof(); + } + + protoboard pb; + { + joinsplit_gadget g(pb); + g.generate_r1cs_constraints(); + g.generate_r1cs_witness( + phi, + rt, + h_sig, + inputs, + out_notes, + vpub_old, + vpub_new + ); + } + + // The constraint system must be satisfied or there is an unimplemented + // or incorrect sanity check above. Or the constraint system is broken! + assert(pb.is_satisfied()); + + // TODO: These are copies, which is not strictly necessary. + std::vector primary_input = pb.primary_input(); + std::vector aux_input = pb.auxiliary_input(); + + // Swap A and B if it's beneficial (less arithmetic in G2) + // In our circuit, we already know that it's beneficial + // to swap, but it takes so little time to perform this + // estimate that it doesn't matter if we check every time. + pb.constraint_system.swap_AB_if_beneficial(); + + std::ifstream fh(pkPath, std::ios::binary); + + if(!fh.is_open()) { + throw std::runtime_error(strprintf("could not load param file at %s", pkPath)); + } + + return ZCProof(r1cs_ppzksnark_prover_streaming( + fh, + primary_input, + aux_input, + pb.constraint_system + )); + } +}; +``` +
+这上面的代码是源码中调用验证的代码,其它的相关代码,都可以依此来查看。相关的代码都在zcash目录中。其实应该详细的把这个目录下的代码分析一下,就会把整个流程弄得更清楚。 + + +## 五、总结 +通过三个链的匿名方式的比较,可以发现,其实区块链在匿名方向上从传统走向新技术的过程。其它的区块链的匿名的方式,或多或少也是如此,在新技术没有得到完全验证的前提下,适当的组合和局部创新便成了主流。 diff --git a/quorum/img/QuorumTransactionProcessing.jpg b/quorum/img/QuorumTransactionProcessing.jpg new file mode 100644 index 0000000..e1f7dcb Binary files /dev/null and b/quorum/img/QuorumTransactionProcessing.jpg differ diff --git a/quorum/img/Quorum_Equity_Use_Case.png b/quorum/img/Quorum_Equity_Use_Case.png new file mode 100644 index 0000000..4e7bad9 Binary files /dev/null and b/quorum/img/Quorum_Equity_Use_Case.png differ diff --git a/quorum/img/architecture.png b/quorum/img/architecture.png new file mode 100644 index 0000000..27e8903 Binary files /dev/null and b/quorum/img/architecture.png differ diff --git a/quorum/img/architecture02.png b/quorum/img/architecture02.png new file mode 100644 index 0000000..988e1c4 Binary files /dev/null and b/quorum/img/architecture02.png differ diff --git a/quorum/img/privacy-design.png b/quorum/img/privacy-design.png new file mode 100644 index 0000000..52f1056 Binary files /dev/null and b/quorum/img/privacy-design.png differ diff --git a/quorum/img/privacy.png b/quorum/img/privacy.png new file mode 100644 index 0000000..1c7c404 Binary files /dev/null and b/quorum/img/privacy.png differ diff --git a/quorum/img/quorum.splite.state.db.png b/quorum/img/quorum.splite.state.db.png new file mode 100644 index 0000000..d1a74e8 Binary files /dev/null and b/quorum/img/quorum.splite.state.db.png differ diff --git a/quorum/img/quorum.startup.jpg b/quorum/img/quorum.startup.jpg new file mode 100644 index 0000000..b15ef00 Binary files /dev/null and b/quorum/img/quorum.startup.jpg differ diff --git a/quorum/quorum.introduction.md b/quorum/quorum.introduction.md new file mode 100644 index 0000000..9678efd --- /dev/null +++ b/quorum/quorum.introduction.md @@ -0,0 +1,134 @@ +# J.P.Morgan Quorum 介绍 + +## 简介 + +Quorum是是一个联盟链方案,由摩根大通开发的企业级分布式账本和智能合约平台,它是在以太坊(Ethereum)的基础上开发的,提供私有智能合约执行方案,并满足企业级的性能要求。适用于需要高速交易以及高吞吐量处理联盟间进行私有交易的应用场景,主要是为了解决了区块链技术在金融及其他行业应用的特殊挑战而设计。 + +Quorum是基于以太坊的Golang版本(go-ethereum)开发而成,为金融服务行业提供以太坊许可链方案,可以方便的支持交易与合约的隐私性。在go-ethereum的基础上,主要的改动点有如下几项: + +- 添加了交易与合约的隐私性:对于一笔交易来说,只有交易的相关方才能看到交易的细节,非相关方是看不到交易细节的 +- 网络/节点的许可管理:由于Quorum不是公链,而是联盟链,这个特点决定了不是任何节点都是能够随意加入区块链的,只有经过授权的节点才能够加入区块链 +- 调整了共识机制:把以太坊原有的PoW调整为基于Raft的共识机制 +- 更高性能:由于共识算法的调整,创建新块的步骤和整个区块链对新块达成共识的速度大大提高 + + +## Quorum的系统架构 + +如下图所示,这是Quorum企业级区块链的系统架构图: + +![](img/architecture.png) + +从架构图中我们可以看到,Quorum是基于以太坊的Golang版本开发。Quorum做设计之初就确定尽可能避免重复造轮子,尽量复用更多的已有技术,尽可能的减少对以太坊的修改,以便于未来合并以太坊升级之后的代码更容易。 + +整个Quorum的系统架构分为上下两层, + +##### 下层是传统的区块链层 + +这一层为整个Quorum提供基础的区块链功能服务,作为上层应用层的基础设施,为上层提供相应的编程接口,使得应用层能够充分的利用底层的区块链功能。 + +从图中可以明显的看出来,这一层包含了三大组件: +1. 基于以太坊的Quorum节点 +2. 事物管理器(Transaction Manager) +3. 飞地(Enclave) + +Quorum节点,就是修改后的以太坊的Golang版本实现,用以存储公共的交易和私有交易。Quorum对geth做了以下改动: + +- 把PoW共识算法,修改成基于Raft的共识算法 +- P2P网络层,修改成只有授权节点才能加入网络 +- 区块生成逻辑,由检查“全局状态root”改为检查“全局公开状态root” +- 区块验证逻辑,在区块头,将“全局状态root”替换成“全局公开状态root” +- 状态树,分成公开状态树和私有状态树 +- 区块链验证逻辑,修改成处理“私有事务” +- 创建事务,修改成允许交易数据被加密哈希替代,以维护必需的隐私数据 +- 删除以太坊中Gas的定价,尽管保留Gas本身 + + +另外就是包含了Quorum添加进来的最重要的Constellation模块,这个模块我们可以想象它是一个分布式的密钥服务器,等同于用PGP加密消息的MTA(消息传输代理)网络,做为一个独立的模块,它可以适用于包括区块链在内的许多类型的应用程序,它是实现Quorum的“隐私引擎”,目前的版本使用Haskell语言编写。 + +Constellation模块又进一步的细分为两个子模块: + +1. 事务管理器(Transaction Manager): +允许访问私有事务的加密交易数据、管理本地数据存储以及与其他事务管理器的通信。它做隐私数据的数据层,提供数据的安全访问并利用Enclave来实现数据的安全加密。 + +2. 飞地(Enclave) +Enclave就是为了解决区块链中记帐的真实性和安全性而实现的一个模块。它通过事务隔离和特定的加密措施来提供并行操作,大大提高了性能。正如上面所讲,隐私数据的安全处理基本都是通过Enclave来实现的。 + +##### 上层是商业逻辑层 + +具体的业务应用程序全部都在这一层,应用层使用底层提供的区块链功能来实现传统的商业业务到区块链系统的移植。这一层又细分为三个子模块: + +1. 智能合约模块(Smart Contracts): +2. DApps模块 +3. 已有的App集成模块 + + +Quorum的解决方案,是使用密码学技术来对私有交易数据加密,以防止交易方以外的人看到敏感数据。这种方案使用一个单独的共享区块链,一个智能合约框架,以及修改后的以太坊;其中智能合约框架对隐私数据进行了隔离。对go-ethereum代码库进行的修改,包括区块提案和验证过程的修改。区块验证过程,是通过执行交易合约代码来进行的,比如所有节点都对公开交易、和与交易方相关的私有交易进行验证;对于其他私有交易,节点将会忽略合约代码的执行过程。 + +这种设计,把状态数据库切分成为了两部分,一部分是公开状态数据库,一部分是私有状态数据库。其中公开状态数据库存储所有公开的数据,比如原始以太坊的所有交易信息都是公开数据。而私有状态数据库则只保存私有的交易信息,而且这类交易信息只有交易的相关方才能对其解密和查看,非交易相关方则无法查看交易的详细信息,这都是通过Constellation模块的加密所完成的。另外,链上所有的事物仍然可以同步到所有节点,为防止篡改和隐私而做了安全加密处理。如下图所示: + +![](img/quorum.splite.state.db.png) + + +## 交易隐私性 + +交易隐私性是Quorum的独有特点,是其核心功能之一。为此,Quorum引入“公开交易”和“私有交易”这两个概念。注意,这只是一个理论上的概念,实际上Quorum并未引入新的事务类型;只是在以太坊事务模型的基础上扩展包含一个可选的privateFor参数,这个私有参数会使得Quorum把这一类事务当做私有事务,另外加入一个IsPrivate方法来识别交易是否是私有事务类型。 + +下面我们以处理私有交易的流程为例解释Quorum的事务流程,如下图所示: + +![](img/QuorumTransactionProcessing.jpg) + +在这个案例中,A和B构成了私有交易AB的交易双方,而C机构不参与该交易。 + +1. A将隐私交易发送到Quorum节点,节点指定交易的有效载荷(内容),并为A和B指定PrivateFor参数为A和B的公钥。 +2. 节点将Tx发送到对应的Transaction Manager并存储相关的交易数据。 +3. Transaction Manager调用加密的Enclave模块相关函数请求对Tx加密。 +4. 节点A的Enclave检验A的私钥,如果验证通过,就进行下列动作: + 1. 第一步,生成一个对称密钥和随机值(说明一下这里使用对称密钥的作用是为了加快速度)。 + 2. 第二步,使用上一步生成的密钥加密Tx及相关内容。 + 3. 第三步,由SHA3-512来计算加密后的Tx的内容的HASH值。 + 4. 第四步,遍历交易方列表(此处为A和B)使用第一步产生的密钥对用第一步中的public key加密,然后生成一个新的值(加密方法PGP)。 + 5. 第五步,将上述二三四步的结果返回给Transaction Manager. +5. A的Transaction Manager使用从 Enclave 中获取的 hash 值作为索引把加密后的TX以及加密后的密钥保存到本地。同时,Transaction Manager会把hash值,加密后的Tx,public_key_B加密的密钥这三项通过HTTPS发送给PartyB的Transaction Manager。PartyB的Transaction Manager收到数据后,会进行ACK/NACK的应答。需要注意的是,如果A没有收到应答,那么交易不会在网上传播,也就是说,接收人存储通信的有效载荷是网络传播的前提条件。 +6. 一旦发往B的Transaction Manager的交易成功,A的事务管理器便将hash值返回给其对应的Quorum节点。该节点用hash值来替换原来Tx的交易内容(有效载荷)。修改Tx的V值为37或者38(Private Transaction的标识)。其他节点可以通过这个V的值来判断其是否为已加密交易内容的相关私有交易;否则,即为一个无意义的字节码相关的公开交易。 +7. 使用标准的以太坊的通信协议将节点通过P2P方式广播给整个网络。 +8. 此交易被打包到区块中,并分发到各个网络用户。 +9. 节点收到这个Tx的区块后,发现这个Tx的V值为37或38。表明这个Tx是隐私的交易,需要解密,需要调用本地的事务管理器,最终决定是否同意这笔交易(使用hash索引查找)。 +10. 因为用户C的节点不能控制这笔Tx,所以它只会接收到一个NotARecipient的消息,从而忽略这笔交易——c用户不会更新自己的私有状态数据库。A和B将会在自己的事务管理器中查找哈希值,识别他们的确同意该交易,然后它们调用对应的Enclave模块,传递已加密交易内容和加密的系统密钥和签名。 +11. Enclave模块验证签名,然后使用在Enclave中保存的该用户的私钥解密对称密钥,使用解密的密钥对交易内容进行解密,然后将解密的交易内容返回给事务管理器。 +12. 用户A和B的事务管理器,将解密的Tx后通过EVM执行。执行完成后将执行结果返回给Quorum节点,并更新Quorum节点的私有状态。注意:一旦代码被执行后将会被抛弃,因此在不经过上述过程的情况下,它无法被读取。 + + +## 基于Raft的共识机制 + +Quorum放弃了以太坊本身的PoW共识算法,而是使用了基于Raft的共识机制,其使用了etcd的Raft实现源代码。Quorum认为对于这类联盟链来说,不存在作恶节点,所以不需要拜占庭容错并且需要更快的出块时间(以毫秒而非秒为单位)和事务最终确认(不存在分叉)。还有一个特点是,这种共识机制不会去创建空白区块,而是按照需要更快、更有效的创建新区块。 + +在geth命令添加 --raft 标志,就会使得geth节点运行raft共识算法。 + +Raft和Ethereum都有自己的“节点”概念,但它们稍微有点儿不一样。 + +在Raft里面,一个“节点”在正常操作的时候,要么是Leader,要么是Follower。在整个集群里面,只有一个Leader,所有其他的节点都要从这个Leader来获取日志数据。这里还有一个Candidate的概念,不过仅仅是在Leader选举期间才有。 + +但是在Ethereum里面却没有Leader和Follower这样的概念,对于任何一个节点来说,都可以创建一个新块(只要计算足够快),这就类似于Raft里面的Leader。 + +在基于Raft的共识算法中,在Raft和Ethereum节点之间做了一对一的对应关系,每个Ethereum节点也是Raft节点,并且按照约定,Raft集群的Leader是产生新块的唯一Ethereum节点。这个Leader负责将交易打包成一个区块,但不提供工作量证明(PoW)。 + +| Ethereum | Raft | +| :-: | :-: | +| minter(旷工) | leader(领导者) | +| verifier(验证者) | follower(追随者) | + +在这里把Leader和产生新块的节点绑定到一起的主要原因有两点:第一是为了方便,因为Raft确保一次只有一个Leader,第二是为了避免从节点创建新块到Leader的网络跳转,所有的Raft写入操作都必须通过该跳转。Quorum的实现关注Raft Leader的变化——如果一个节点成为Leader,它将开始产生新块,如果一个节点失去Leader地位,它将停止产生新块。 + +Quorum使用现有的Etherum P2P传输层来负责在节点之间的通讯,但是只通过Raft的传输层来传输Block。它们是由Leader创造的,并从那里传输到集群的其余部分,总是以相同的顺序通过Raft传输。 + +当Leader创建新块时,不像在Ethereum中,块被写入数据库并立即成为链的新Head,只在新块通过Raft传输之后才插入块或将其设置为链的新Head。所有节点都会在锁定步骤中将链扩展到新的状态,就好像是他们在Raft中同步日志。 + +## 节点授权与安全 + +节点授权,是用来控制哪些节点可以连接到指定节点、以及可以从哪些指定节点移除的功能。目前,当启动节点的时候,通过指定--permissioned参数在节点级别处进行管理。 + +如果指定了--permissioned参数,节点将查找名为permissioned-nodes.json的文件。此文件包含此节点可以连接并接受来自其连接的enodes白名单。因此,启用权限后,只有permissioned-nodes.json文件中列出的节点成为网络的一部分。 如果指定了--permissioned参数,但没有节点添加到permissioned-nodes.json文件,则该节点既不能连接到任何节点也不能接受任何接入的连接。 + + +## 总结 +Quorum通过对网络节点的授权管理,使得只有被授权的节点才能够加入网络;对共识算法的调整,使得Quorum拥有更高的速度和性能;创新的隐私模型,尤其私有交易的处理,使得交易的隐私性大大加强。这一切都使得Quorum非常适合为金融场景服务。 diff --git a/quorum/quorum.node.permission.md b/quorum/quorum.node.permission.md new file mode 100644 index 0000000..2f8f52f --- /dev/null +++ b/quorum/quorum.node.permission.md @@ -0,0 +1,147 @@ +# J.P.Morgan Quorum 节点授权管理 + +## 简介 + +Quorum的网络节点授权,是用来控制哪些节点可以连接到指定节点、以及可以从哪些指定节点移除的功能。目前,当启动geth节点时,通过加入--permissioned命令行参数在节点级别处进行管理。 + +如果设置了--permissioned参数,节点将查找名为/permissioned-nodes.json的文件。此文件包含此节点可以连接并接受来自其连接的enodes白名单。因此,启用权限后,只有permissioned-nodes.json文件中列出的节点成为网络的一部分。 如果指定了--permissioned参数,但没有节点添加到permissioned-nodes.json文件,则该节点既不能连接到任何节点也不能接受任何接入的连接。 + +如果设置了--permissioned参数,但permissioned-nodes.json文件为空或者仅存在于节点的文件夹中,则该节点将启动,但它不会连接到任何其他节点,也不会接受来自其他节点的任何接入连接请求。无论是哪种情况,都期望看到错误记录。 + +permissioned-nodes.json文件包含一个节点参数列表(enode://nodeid@ip:port),指定该特定节点将接受来自连接的接入连接并进行主动拨出连接。 + +permissioned-nodes.json格式如下: + + ["enode://nodeid1@ip1:port1", "enode://nodeid2@ip2:port2", "enode://nodeid3@ip3:port3", ] +例如:(便于查看,节点id仅截取部分展示) + +["enode://8475a01f62a1948126dc1f0d22ecaaaf77e[::]:30301", "enode://c5660501f496360e49ded734a889c98b7da[::]:30302","enode://84bd7df4bda71fb90493cf4706455335919[::]:30303"] +以上将确保此节点只能接受来自/到达此白名单中3个节点的接入或接出连接。 + + +geth选项列表下,--permissioned参数可用: + +$ geth --help +QUORUM OPTIONS: + --permissioned If enabled, the node will allow only a defined list of nodes to connect + + +添加新节点: + +任何添加到permissioned-nodes.json文件的内容,都将在后续发出接入或接出请求时,被服务器动态获取。节点不需要重新启动以便更改生效。 + +删除现有节点: + +从permissioned-nodes.json文件中删除现有的连接节点,不会立即删除那些现有的连接节点。但是,如果连接由于任何原因而被断开,并且随后的连接请求将从被删除的节点id中产生,它将作为新请求的一部分被拒绝。 + + +## 源代码解读 + +在Server struct这个结构体中,添加了EnableNodePermission字段,用来标识是否开启了网络节点的权限管理。实际上这个标识就是在启动geth命令的时候,如果在命令行传递了--permissioned参数,则这个标识为true,否则就是false。 + +```go +// quorum/cmd/utils/flags.go +// 这里展示了对命令行参数的定义 +var ( + // Quorum + EnableNodePermissionFlag = cli.BoolFlag{ + Name: "permissioned", + Usage: "If enabled, the node will allow only a defined list of nodes to connect", + } +) + +// quorum/cmd/utils/flags.go +// 这里是解析命令行参数 +// SetNodeConfig applies node-related command line flags to the config. +func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { + cfg.EnableNodePermission = ctx.GlobalBool(EnableNodePermissionFlag.Name) +} +``` + +Server.SetupConn 这个函数是在网络链接的过程中,执行握手协议,并且尝试添加这个网络链接作为一个peer。在这个函数中,在设置connection的时候,去判断是否启动了节点授权,如果启动了,就去读取相应的节点授权列表 + +```go +// quorum/p2p/server.go +func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) { + if srv.EnableNodePermission { + log.Trace("Node Permissioning is Enabled.") + node := c.id.String() + direction := "INCOMING" + if dialDest != nil { + node = dialDest.ID.String() + direction = "OUTGOING" + log.Trace("Node Permissioning", "Connection Direction", direction) + } + + if !isNodePermissioned(node, currentNode, srv.DataDir, direction) { + return + } + } +} +``` + +在判断一个节点是不是授权节点的时候,就用到了授权节点的配置文件,下面是读配置文件并且判断节点的代码。 + +```go +// quorum/p2p/permissions.go +// check if a given node is permissioned to connect to the change +// 这里是判断一个指定的节点是否被授权加入到这个网络 +func isNodePermissioned(nodename string, currentNode string, datadir string, direction string) bool { + + var permissionedList []string + nodes := parsePermissionedNodes(datadir) + for _, v := range nodes { + permissionedList = append(permissionedList, v.ID.String()) + } + + for _, v := range permissionedList { + if v == nodename { + return true + } + } + return false +} + +// quorum/p2p/permissions.go +// 这里就是去读取permissioned-nodes.json配置文件 +func parsePermissionedNodes(DataDir string) []*discover.Node { + + path := filepath.Join(DataDir, PERMISSIONED_CONFIG) + // Load the nodes from the config file + blob, err := ioutil.ReadFile(path) + + nodelist := []string{} + if err := json.Unmarshal(blob, &nodelist); err != nil { + return nil + } + // Interpret the list as a discovery node array + var nodes []*discover.Node + for _, url := range nodelist { + nodes = append(nodes, node) + } + return nodes +} +``` + + +判断一个节点是不是授权节点的时机,无非就是两个,第一个时机是主动去链接另外一个节点的时候,另外一个时机就是被动的接受另外一个节点的链接,在这两个时间点都做好判断,就可以控制住节点授权的问题,把非授权节点挡在大门外。下面的代码就展示了这两个时间点的操作。 + +```go +// quorum/p2p/dial.go +// dial 函数就是主动的去链接另外一个节点 +func (t *dialTask) dial(srv *Server, dest *discover.Node) bool { + fd, err := srv.Dialer.Dial(dest) + mfd := newMeteredConn(fd, false) + srv.SetupConn(mfd, t.flags, dest) + return true +} + +// quorum/p2p/dial.go +// 在这里就是监听其他节点的链接请求,收到请求之后,就去验证节点是否被授权 +func (srv *Server) listenLoop() { + go func() { + srv.SetupConn(fd, inboundConn, nil) + slots <- struct{}{} + }() +} +``` diff --git a/quorum/quorum.raft.md b/quorum/quorum.raft.md new file mode 100644 index 0000000..c91e6d5 --- /dev/null +++ b/quorum/quorum.raft.md @@ -0,0 +1,332 @@ + +# J.P.Morgan Quorum 共识算法 + +## 简介 + +J.P.Morgan的Quorum是在Ethereum的基础上修改的,他们的理念之一就是,不要重复造轮子,小编很是认可这个理念。他们把Ethereum的PoW共识算法修改成了Raft算法,并且使用了etcd的Raft实现。由于Quorum是用于企业级分布式账本和智能合约平台,提供私有智能合约执行方案,是联盟链方案,而不是公链。所以项目方认为,在这种场景下,拜占庭容错是不需要的,分叉也是不会存在的。取而代之的是,需要更快的出块时间和交易确认。这种共识算法还不会产生出“空快”,并且在需要的时候可以快速有效的创建出新块。 + +在geth命令添加 --raft 选项,就会使得geth节点运行raft共识算法。 + +## 几个基本概念 + +Raft和Ethereum都有自己的“节点”概念,但它们稍微有点儿不一样。 + +在Raft里面,一个“节点”在正常操作的时候,要么是Leader,要么是Follower。在整个集群里面,只有一个Leader,所有其他的节点都要从这个Leader来获取日志数据。这里还有一个Candidate的概念,不过仅仅是在Leader选举期间才有。 + +但是在Ethereum里面却没有Leader和Follower这样的概念,对于任何一个节点来说,都可以创建一个新块(只要计算足够快),这就类似于Raft里面的Leader。 + +在基于Raft的共识算法中,在Raft和Ethereum节点之间做了一对一的对应关系,每个Ethereum节点也是Raft节点,并且按照约定,Raft集群的Leader是产生新块的唯一Ethereum节点。这个Leader负责将交易打包成一个区块,但不提供工作量证明(PoW)。 + +| Ethereum | Raft | +| :-: | :-: | +| minter(旷工) | leader(领导者) | +| verifier(验证者) | follower(追随者) | + +在这里把Leader和产生新块的节点绑定到一起的主要原因有两点:第一是为了方便,因为Raft确保一次只有一个Leader,第二是为了避免从节点创建新块到Leader的网络跳转,所有的Raft写入操作都必须通过该跳转。Quorum的实现关注Raft Leader的变化——如果一个节点成为Leader,它将开始产生新块,如果一个节点失去Leader地位,它将停止产生新块。 + +在Raft的Leader转换期间,其中有一小段时间,有多个节点可能假定自己具有产生新块的职责;本文稍后将更详细地描述如何保持正确性。 + +Quorum使用现有的Etherum P2P传输层来负责在节点之间的通讯,但是只通过Raft的传输层来传输Block。这些Block是由Leader创造的,并从那里传输到集群的其余部分,总是以相同的顺序通过Raft传输。 + +当Leader创建新块时,不像在Ethereum中,块被写入数据库并立即成为链的新Head,只在新块通过Raft传输之后才插入块或将其设置为链的新Head。所有节点都会在锁定步骤中将链扩展到新的状态,就好像是他们在Raft中同步日志。 + +从Ethereum的角度来说,Raft是通过实现 node/service.go 文件中的 Service 接口而集成的。一个独立的协议可以通过这个 Service 接口,注册到节点里面。 + +```go +// quorum/cmd/geth/config.go +func makeFullNode(ctx *cli.Context) *node.Node { + if ctx.GlobalBool(utils.RaftModeFlag.Name) { + // 在这里判断,如果是raft mode,则注册raft service + RegisterRaftService(stack, ctx, cfg, ethChan) + } +} + +func RegisterRaftService(stack *node.Node, ctx *cli.Context, cfg gethConfig, ethChan <-chan *eth.Ethereum) { + // 在这里把raft service注册到node里面去 + if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + // 调用raft.New创建raft service,这个RaftService实现了node.Service接口 + return raft.New(ctx, ethereum.ChainConfig(), myId, raftPort, joinExisting, blockTimeNanos, ethereum, peers, datadir) + }); err != nil { +} +``` + +![](img/quorum.startup.jpg) + + +## 一笔交易的生命周期 + +现在,让我们来看看一个典型的交易的生命周期 + +##### 在任意一个节点上(挖矿者或者验证者) +1. 通过RPC接口向geth提交一笔交易 +2. 利用Ethereum现有的交易传播机制,把交易广播给所有的节点。同时,因为当前集群都被配置成为“静态节点”模式,所以每一个交易都会被发送给集群中的所有节点 + +##### 在挖矿者节点 +3. 挖矿节点接收到交易之后,通过把这个交易加入交易池(transaction pool)的方式加入到下一个block中 +4. 创建新块的工作将会触发一个NewMinedBlockEvent事件,Raft协议管理者通过订阅了minedBlockSub来接收这个事件。在raft/handler.go文件中的minedBroadcastLoop方法会把这个新块发送到ProtocolManager.proposeC channel. + +下面是 NewMinedBlockEvent 事件的定义 +```go +// quorum/core/events.go +type NewMinedBlockEvent struct{ Block *types.Block } +``` + +下面的三个代码块展示了,订阅事件,创建新块的时候触发事件,以及在接收端转发这个事件。 + +```go +// quorum/raft/handler.go +func (pm *ProtocolManager) Start(p2pServer *p2p.Server) { + pm.p2pServer = p2pServer + pm.minedBlockSub = pm.eventMux.Subscribe(core.NewMinedBlockEvent{}) + pm.startRaft() + go pm.minedBroadcastLoop() +} + +// quorum/miner/worker.go +func (self *worker) wait() { + for { + mustCommitNewWork := true + for result := range self.recv { + // Broadcast the block and announce chain insertion event + self.mux.Post(core.NewMinedBlockEvent{Block: block}) + } + } +} + +// quorum/raft/handler.go +func (pm *ProtocolManager) minedBroadcastLoop() { + for obj := range pm.minedBlockSub.Chan() { + switch ev := obj.Data.(type) { + case core.NewMinedBlockEvent: + select { + case pm.blockProposalC <- ev.Block: + case <-pm.quitSync: + return + } + } + } +} + +``` + +5. serveLocalProposals在这个channel的出口处等待接收这个新块,它的任务是使用RLP的方式对这个block进行编码并且提交给Raft协议。一旦这个新块通过Raft的同步协议同步到了所有的节点,这个新块就成为整个链的最新Head。下面的代码块展示了这个过程。 + +```go +// quorum/raft/handler.go +func (pm *ProtocolManager) serveLocalProposals() { + for { + select { + case block, ok := <-pm.blockProposalC: + size, r, err := rlp.EncodeToReader(block) + var buffer = make([]byte, uint32(size)) + r.Read(buffer) + // blocks until accepted by the raft state machine + pm.rawNode().Propose(context.TODO(), buffer) + } + } +} +``` + +##### 在任意一个节点上 + +6. 到了这个时间点,Raft协议会达成共识并且把包含新块的日志记录添加到Raft日志之中。Raft完成这一步是通过Leader发送AppendEntries给所有的Follower,并且所有的Follower对这个消息进行确认。一旦Leader收到了超过半数的确认消息,它就通知每一个节点,这个新的日志已经被永久性的写入日志。 +7. 这个新块通过Raft传输到整个网络之后,到达了eventLoop,在这里处理Raft的新日志项。他们从Leader通过pm.transport(rafthttp.Transport的一个instance)到达。 + +```go +// quorum/raft/handler.go +func (pm *ProtocolManager) eventLoop() { + for { + select { + case <-ticker.C: + // when the node is first ready it gives us entries to commit and messages + // to immediately publish + case rd := <-pm.rawNode().Ready(): + + // 1: Write HardState, Entries, and Snapshot to persistent storage if they + // are not empty. + pm.raftStorage.Append(rd.Entries) + + // 2: Send all Messages to the nodes named in the To field. + pm.transport.Send(rd.Messages) + + // 3: Apply Snapshot (if any) and CommittedEntries to the state machine. + for _, entry := range pm.entriesToApply(rd.CommittedEntries) { + switch entry.Type { + case raftpb.EntryNormal: + var block types.Block + err := rlp.DecodeBytes(entry.Data, &block) + + if pm.blockchain.HasBlock(block.Hash(), block.NumberU64()) { + } else { + pm.applyNewChainHead(&block) + } + + pm.advanceAppliedIndex(entry.Index) + } + + case <-pm.quitSync: + return + } + } +} + +``` + +8. 下一步是applyNewChainHead会处理这个新块。这个方法首先会检查这个新块是否扩展了链(比如:其parent是当前链的head)。如果这个新块没有扩展链,他会被简单的忽略掉。如果这个新块扩展了链,并且这个新块是有效的,则会通过InsertChain把这个新块写入链中并且作为链的Head. + +```go +// quorum/raft/handler.go +func (pm *ProtocolManager) applyNewChainHead(block *types.Block) { + if !blockExtendsChain(block, pm.blockchain) { + headBlock := pm.blockchain.CurrentBlock() + + pm.minter.invalidRaftOrderingChan <- InvalidRaftOrdering{headBlock: headBlock, invalidBlock: block} + } else { + if existingBlock := pm.blockchain.GetBlockByHash(block.Hash()); nil == existingBlock { + if err := pm.blockchain.Validator().ValidateBody(block); err != nil { + panic(fmt.Sprintf("failed to validate block %x (%v)", block.Hash(), err)) + } + } + _, err := pm.blockchain.InsertChain([]*types.Block{block}) + } +} + + +// quorum/core/blockchain.go +func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { + n, events, logs, err := bc.insertChain(chain) + bc.PostChainEvents(events, logs) + return n, err +} +``` + +9. 通过发送一个ChainHeadEvent事件来通知所有的listener,这个新块已经被接受了。因为下面这些原因,这个步骤是非常重要的: + 1. 从交易池(transaction pool)中删除相关的交易 + 2. 从speculativeChain的proposedTxes中删除相关的交易 + 3. 触发requestMinting(在minter.go文件中)事件,通知节点准备创建新块 + +```go +// quorum/core/blockchain.go +func (bc *BlockChain) PostChainEvents(events []interface{}, logs []*types.Log) { + for _, event := range events { + switch ev := event.(type) { + case ChainEvent: + bc.chainFeed.Send(ev) + + case ChainHeadEvent: + bc.chainHeadFeed.Send(ev) + + case ChainSideEvent: + bc.chainSideFeed.Send(ev) + } + } +} +``` + +现在, 该交易在群集中的所有节点上都可用, 并且最终确认了。因为Raft保证了存储在其日志中的条目的单一顺序, 而且由于所提交的所有内容都保证保持不变, 所以没有blockchain在Raft上生成的分叉。 + + +## 链延长、竞争和纠错 + +Raft负责达成共识, 有哪些区块可以被链接受。在最简单的情况下, 通过Raft的每个后续块都成为新的链Head。 + +然而, 在一些比较极端的情况下, 可能会遇到一个新的块, 已经通过Raft传播到整个集群,但却不能作为新的链Head。在这种情况下, 利用Raft的日志顺序, 如果我们遇到一个块, 其parent目前不是链的Head, 我们只是简单地跳过这个日志条目。 + +最常见的情况是, 在Leader发生变化时, 最有可能触发这种情况。领导者可以被认为是一个代理,这个代理应该创建新块,这通常都是正确的, 并且只有一个单一的新块创建者。但是不能依赖于一个新块创建者的最大并发量来保持正确性。在这样的过渡过程中, 两个节点可能会在短时间内都会创建新块。在这种情况下, 将会有一场竞赛, 成功扩展链条的第一块将会获胜, 竞赛的失败者将被忽略。 + +请考虑下面的示例, 在这种情况下, Raft试图延长链的日志项被表示为: + +[ 0xa12345 Parent: 0xea097c ] + +其中0xa12345是新块的id, 0xea097c是其parent的id。这里初始的挖矿节点(节点1)被分区, 节点2作为后续挖矿节点接管挖矿工作。 + + +新块提交过程: + +链的初始状态:[ 0xa12345 Parent: 0xea097c ] + + + | 步骤 | 节点1 | 节点2 | + | :-: | :-: | :-: | + | 1 | 发生网络分区,节点1被划分到另外一个小区 | 节点2被选择成为新的Leader(挖矿者) | + | 2 | [ 0x90f72a Parent: 0xa12345 ] | [ 0x69c92376 Parent: 0xa12345 ] | + | 3 | | [ 0xb7239ae Parent: 0x69c92376 ] | + | 4 | 被拒绝 | | + | 5 | | [ 0x73a896c Parent: 0xb7239ae ] | + + +一旦网络分区愈合, 在Raft层节点1将重新提交0x90f72a, 结果序列化日志可能看起来如下: + +- [ 0xea097c Parent: 0xacaa - 挖矿成功 ] (节点1) +- [ 0xa12345 Parent: 0xea097c - 挖矿成功 ] (节点2; 竞赛获胜者) +- [ 0x69c92376 Parent: 0xa12345 - 挖矿成功 ] (节点2) +- [ 0xb7239ae Parent: 0x69c92376 - 挖矿成功 ] (节点2) +- [ 0x90f72a Parent: 0xa12345 - 挖矿失败,没有操作 ] (节点1; 竞赛失败者) +- [ 0x73a896c Parent: 0xb7239ae - 挖矿成功 ] (节点2) + + +由于被序列化后的"赢家"将会延长链, 所以"失败者"将不会延长链, 因为它的parent(0xea097c)已经不是链的head了, 竞赛"获胜者"已经提前延长了同一个parent(0xa12345),然后0xb7239ae进一步延长了它。 + +请注意, 每个块都被Raft接受并在日志中序列化, 并且这个失败者的延长被"忽略"。从Raft的角度来看, 每个日志条目都是有效的, 但在Quorum-Raft的角度看, 将会选择使用哪些条目作为有效条目, 并且在实际上将延长链。此链的延长逻辑是确定性的: 在群集中的每个节点上都会发生相同的精确行为, 从而保持blockchain同步。 + +还要注意Quorum的方法不同于Ethereum的"最长有效链"(LVC:Longest Valid Chain)机制。LVC用于在最终一致的网络中解决分叉问题。因为Quorum使用Raft, blockchain的状态是保持一致的。Raft设置中不能分叉。一旦一个块被添加为新的链Head, 对于整个集群来说都是这样的,而且它是永久性的。 + +## 创建新块的频率 + +默认情况下, 创建新块的频率是50ms。当新的交易来了, 将立即创建一个新块(所以延迟时间很低), 但是新块的创建时间至少也是上一个块创建的50ms之后。这样的频率是在交易速度和延迟之间获取一个平衡。 + +50ms这个频率是可以通过参数--raftblocktime配置。 + +## 预测挖矿 + +Quorum的方法不同于Ethereum的方法之一,是引入了一个新的概念"预测挖矿"。对基于Raft的Quorum的共识算法来说, 这并不是严格要求的, 而是一个优化, 它提供了降低创建新块之间的时间延迟,或者说是更快的最终确认时间。 + +通过基于Raft的共识算法,新块可以更快的成为链的Head。如果在创建新块之前,所有的节点都同步等待上一个块成为新的链头,那么这个集群收到的任何交易都需要更多的时间才能使其进入链。 + +在预测挖矿中,我们允许一个parent块通过Raft进入块链之前,创建一个新块。 + +由于这个过程可能重复发生,这些块(每个都有一个对其父块的引用)可以形成一种链。称之为“预测链”。 + +在预测链形成的过程中,Quorum会持续跟踪交易池中的事务子集,这些事务子集已经加入到块中,只是这些块还没有放入到链中而是在预测链中)。 + + +由于竞赛的存在(如我们上面所详细描述的),有可能预测链的中间某些区块最终不会进入到链。在这种情况下,将会触发一个InvalidRaftOrdering事件,并且相应地清理预测链的状态。 + +这些预测链的长度目前还没有限制,但在未来可能会增加对这一点的支持。 + + +## 预测链的状态 +- head:这是最后一个创建的预测区块,如果最后一个创建的block已经包含在区块链中,这个值可以是nil +- proposedTxes:这是一个交易的集合,这些交易已经被打包到一个block中,并且这个block已经提交到Raft协议,但是这个block还没有加入到链中 +- unappliedBlocks:这是一个block的队列,这些block已经提交到Raft协议,但是这些block还没有加入到链中 + - 当创建一个新块的时候,这个新块会被添加到这个队列的尾部 + - 当一个新块被添加到链中以后,accept方法会被调用来把这个blokc从这个队列删除 + - 当一个InvalidRaftOrdering事件发生的时候,通过从队列的“最新的末尾”弹出最新的块,直到找到无效的块来展开队列。我们必须重复地删除这些“新”的预测块,因为它们都依赖于一个没有被包括在链中的block。 +- expectedInvalidBlockHashes:在无效块上建立的一组块,但尚未通过Raft传递。这些块要被删除。当这些不延伸的块通过Raft回来时,会把它们从预测链中移除。在不应该去尝试预测链的时候,这一套方法就成为一种保护机制。 + +## Raft传输层 + +Quorum通过Raft(etcd实现)内置的HTTP传输方法来传输block,从理论上来说,使用Ethereum的P2P网络来作为Raft的传输层也是可以的。在实际的测试中,在高负载的情况下,Raft内置的HTTP传输方法比geth中内置的P2P网络更为可靠。 + +在缺省情况下,Quorum监听50400端口,这个也可以通过--raftport参数来做配置。 + +缺省的peers数量被设置为25。最大的peers数量可以通过--maxpeers来做配置,这个数量也是整个集群的数量。 + + +## 初始化配置 + +当前基于Raft的共识算法,要求所有的初始节点都要配置为把前面所有的其他节点都作为静态节点对待。对每一个节点来说,这些静态节点的URI必须包含在raftport参数中,比如:enode://abcd@127.0.0.1:30400?raftport=50400 + +注意:所有节点的static-nodes.json文件中,enodes的顺序必须保持一致。 + +想要从一个集群中删除一个节点,那就进入JavaScript控制台,执行命令:raft.removePeer(raftId),这个raftId就是你想要删除的节点id。对于初始节点来说,这个id是在静态节点列表中的索引值,这个索引值是从1开始的(不是从0开始)。一旦一个节点从集群中删除了,这个是永久性的删除。这个raftId在将来也不能够使用。如果这个节点想要再次加入集群,那么它必须使用一个新的raftId。 + +想要把一个节点加入到集群,那就进入JavaScript控制台,执行raft.addPeer(enodeId)命令。就像enode ID需要包含在静态节点JSON文件中一样,这个enode ID也必须要包含在raftport参数中。这个命令会分配一个新的raftID,并且返回。成功执行addPeer命令之后,就可以启动一个新的geth节点,并且添加参数 --raftjoinexisting RAFTID + + +## 小结 + +通过这篇文章对Quorum共识机制的介绍,我们可以看到,Quorum对于适合于自己的目标场景有着非常清晰的理解和认识,从而把Ethereum原生的PoW修改成适用于企业级的联盟链平台。 + + +------------- diff --git "a/quorum/quorum\351\232\220\347\247\201\345\210\206\346\236\220.md" "b/quorum/quorum\351\232\220\347\247\201\345\210\206\346\236\220.md" new file mode 100644 index 0000000..2c18869 --- /dev/null +++ "b/quorum/quorum\351\232\220\347\247\201\345\210\206\346\236\220.md" @@ -0,0 +1,743 @@ +# quorum 的隐私分析 + +# 一、隐私性设计 +privacy(隐私性)是Quorum的重要的部分,Quorum的一个重要特点就是在以太坊的基础上增加了隐私性(参看白皮书)。将交易和数据进行了隐私性隔离,包括加密和零知识证明等。在将隐私性相关抽象出来以后,导致的一个结果就是,状态数据库的分裂。 +
+在以太坊中,MTP主宰的状态树控制着整个以太坊的世界,任何的风吹草动,都必须纳入状态数据库的更新。但是在Quorum中,公有的数据仍然保持在全局状态的更新,但是私有的数据不被更新到全局状态中,而是被加密保存到节点上,同样通过分布式的事务等同步到所有的节点上。 +
+ +![privacy](img/privacy.png) +
+通过上图可以看到,隐私分成了两部分即 ZSL和Constellation两部分。 +
+ZSL:Zero-knowledge Security Layer,即零知识证明安全层。它主要使用ZCASH的零知识证明的zk-SNARKs来增强Constellation机制。 +
+Constellation:P2P的加密消息交换系统,提供整个Quorum的信息安全交换的模块。\ +
+一个私有交易的简单流程: +
+ +![privacy-d](img/privacy-design.png) +
+合约交易状态: +
+下面的交易是允许的: +
+1) S -> A -> B +
+2) S -> (A) -> (B) +
+3) S -> (A) -> [B -> C] +
+ +而下面的交易是不支持的: +
+1) (S) -> A +
+2) (S) -> (A) + +说明: + S = sender +
+ (X) = private +
+ X = public +
+ -> = direction +
+ [] = read only mode + +
+通过上述的分析可以看出,Quorum的隐私设计是基于加密的安全算法来实现的。不管是在ZSL还是在Constellation,它都提供了相关的Token来进行相应的支持。 +
+ +## 二、Constellation +
+Constellation模块 +
+在Quorum的架构文档中可以看到,Constellation是其重要的一个模块。你可以想象它是一个分布式的密钥服务器,等同于用PGP加密消息的MTA(消息传输代理)网络,做为一个独立的模块,它可以适用于包括区块链在内的许多类型的应用程序,它是实现Quorum的“隐私引擎”,目前使用的版本是使用Haskell实现。 +
+Constellation模块由两个子模块组成: +
+ +![arch](img/architecture.png) +
+ +### 1、事务管理器(Transaction Manager): +
+事务管理器,允许访问私有事务的加密交易数据、管理器本地数据存储以及与其他事务管理器的通信。它做隐私数据的数据层,提供数据的安全访问并利用Enclave来实现数据的安全加密。 +
+ +### 2、飞地(Enclave) +
+Enclave就是为了解决区块链中记帐的真实性和安全性而实现的一个模块。它通过事务隔离和特定的加密措施来提供并行操作,大大提高了性能。正如上面所讲,隐私的数据的安全处理基本都是通过Enclave来实现的。 +
+ +## 三、ZSL +
+ZSL在Quorum中是单独提供的,它提供了两种支持的方法,一种是直接在其JS平台中调用相关的接口,另外一种是使用C++ libsnark library提供的基本库来供Golang使用。具体的使用办法和接口参看下面的地址: +
+(https://github.com/jpmorganchase/zsl-q/blob/master/README.md) +
+在Quorum中,1.5和1.6版本都提供了对ZSL的支持,但在最新的版本中,没有提供对ZSL的支持。猜测可能是JP想把ZSL专门做为一个特别的部分来提供相应的服务,而不是广泛的集成到整体的Quorum这个项目中去。 +
+下图是一个相关的简单股权交易用例: +
+ +![ZSL](img/Quorum_Equity_Use_Case.png) + +
+ZSL的使用还是比较复杂的,需要下载相关的库和相关的模块。如果想完整运行一个ZSL的Quorum项目,请参照下面的地址: +
+https://github.com/jpmorganchase/zsl-q/blob/master/README.md +
+ +## 四、源码分析 + +### 1、隐私交易流程介绍 +
+Quorum的交易有两类即”Public Transaction” 和 “Privat Transaction”,也就是公共交易类型和隐私交易类型。在实际的交易中,前者完全兼容以太坊的交易。而在后者的隐私交易中,在原有以太坊的 Transaction 模型上,进行了部分修改。在Quorum 交易tx 模型基础上增加了 “privateFor” 字段。另外在还其内部增加了一个判断是否公共或者隐私交易类型的方法 “IsPrivate”。 +
+先看一个官方WIKI上的隐私交易的流程图,这个图中,一笔交易与用户A和用户B有关系,但是和用户C没有关系: +
+ +![private-t](img/QuorumTransactionProcessing.jpg) +
+说明: +
+1)、用户A将隐私交易发送到Quorum节点,节点指定交易的有效载荷(内容),并为A和B指定PrivateFor参数为A和B的公钥。 +
+2)、节点将Tx发送到对应的Transaction Manager并存储相关的交易数据。 +
+3)、Transaction Manager调用加密的Enclave模块相关函数请求对Tx加密。 +
+4)、节点A的Enclave检验A的私钥,如果验证通过,就进行下列动作: +
+第一步,生成一个对称密钥和随机值(说明一下这里使用对称密钥的作用是为了加快速度)。 +
+第二步,使上一步生成的密钥加密Tx及相关内容。 +
+第三步,由SHA3-512来计算加密后的Tx的内容的HASH值。 +
+第四步,遍历交易方列表(此处为A和B)使用第一步产生的密钥对用第一步中的public key加密,然后生成一个新的值(加密方法PGP)。 +
+第五步,将上述二三四步的结果返回给Transaction Manager. +
+5)、A的Transaction Manager使用从 Enclave 中获取的 hash 值作为索引把加密后的TX以及加密后的密钥保存到本地。同时,Transaction Manager会把hash值,加密后的TX,public_key_B加密的密钥这三项通过HTTPS发送给PartyB的Transaction Manager。PartyB的Tx Manager收到数据后,会进行ACK/NACK的应答。需要注意的同如果A没有收到应答,那么交易不会在网上传播,也就是说,接收人存储通信的有效载荷是网络传播的前提条件。 +
+6)、一旦发往B的Transaction Manager的交易成功,A的事务管理器便将hash值返回给其对应的Quorum节点。该节点用hash值来替换原来TX的交易内容(有效载荷)。修改TX的 V 值为 37 或者 38(Private Transaction的标识)。其他节点可以通过这个 V 的值来判断其是否为已加密交易内容的相关私有交易;否则,即为一个无意义的字节码相关的公开交易。 +
+7)、使用标准的以太坊的通信协议将节点通过P2P方式广播给整个网络。 +
+8)、此交易被打包到区块中,并分发到各个网络用户。 +
+9)、节点收到这个TX的区块后,发现这个TX的 V 值为37或38。表明这个Tx是隐私的交易,需要解密,需要调用本地的事务管理器,最终决定是否同意这笔交易(使用hash索引查找)。 +
+10)、因为用户C的节点不能控制这笔TX,所以它只会接收到一个NotARecipient的消息,从而忽略这笔交易——c用户不会更新自己的私有状态数据库。A和B将会在自己的事务管理器中查找哈希值,识别他们的确同意该交易,然后它们调用对应的Enclave模块,传递已加密交易内容和加密的系统密钥和签名。 +
+11)、Enclave模块验证签名,然后使用在Enclave中保存的该用户的私钥解密对称密钥,使用解密的密钥对交易内容进行解密,然后将解密的交易内容返回给事务管理器。 +
+12)、用户A和B的事务管理器,将解密的TX后通过EVM执行。执行完成后将执行结果返回给Quorum节点,并更新Quorum节点的私有状态。注意:一旦代码被执行将会无效,因此在不经过上述过程的情况下,它无法被读取。 +
+ +### 2、隐私交易流程的源码 +
+隐私交易的部分在Quorum中只有Transaction Manager部分,Enclave部分是在另外的项目constellation中,用haskell语言编写完成。下面分别介绍这两个部分。 +
+在了解了私有交易的流程后,看一下相关的代码: +
+1)、隐私交易 +
+ +``` go +func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { +...... + isPrivate := args.PrivateFor != nil + + if isPrivate { + //判断私有交易 + //Send private transaction to local Constellation node + log.Info("sending private tx", "data", fmt.Sprintf("%x", data), "privatefrom", args.PrivateFrom, "privatefor", args.PrivateFor) + //这里会调用下面的PrivateTransactionManager的Send,二者就联系到一起了 + data, err = private.P.Send(data, args.PrivateFrom, args.PrivateFor) + log.Info("sent private tx", "data", fmt.Sprintf("%x", data), "privatefrom", args.PrivateFrom, "privatefor", args.PrivateFor) + if err != nil { + return common.Hash{}, err + } + args.Data = data + } + +...... + //处理Quorum交易相关值,见上面流程说明 + isQuorum := tx.IsPrivate() +...... +//签名和加密 + signed, err := wallet.SignTx(account, tx, chainID, isQuorum) +...... + return submitTransaction(ctx, s.b, signed, isPrivate) +} + +//交易相关 +// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool. +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Nonce *hexutil.Uint64 `json:"nonce"` + + PrivateFrom string `json:"privateFrom"` + //PrivateFor这个关键字段 + PrivateFor []string `json:"privateFor"` +} +//增加的是否为私有交易判断 +func (m Message) IsPrivate() bool { + return m.isPrivate +} + +func (tx *Transaction) IsPrivate() bool { + if tx.data.V == nil { + return false + } + return tx.data.V.Uint64() == 37 || tx.data.V.Uint64() == 38 +} + +func (tx *Transaction) SetPrivate() { + //设置V的值,见上面的流程 + if tx.data.V.Int64() == 28 { + tx.data.V.SetUint64(38) + } else { + tx.data.V.SetUint64(37) + } +} +``` +
+ +2)、Transaction Manager模块 +
+这个模块包含private.go和node.go以及constellation.go几个模块。在private.go中实现了: +
+ +``` go +type PrivateTransactionManager interface { + Send(data []byte, from string, to []string) ([]byte, error) + Receive(data []byte) ([]byte, error) +} +``` +
+而在Constellation这个结构体中实现了这个接口: +
+ +``` go +func (g *Constellation) Send(data []byte, from string, to []string) (out []byte, err error) { + if g.isConstellationNotInUse { + return nil, ErrConstellationIsntInit + } + out, err = g.node.SendPayload(data, from, to) + if err != nil { + return nil, err + } + g.c.Set(string(out), data, cache.DefaultExpiration) + return out, nil +} + +func (g *Constellation) Receive(data []byte) ([]byte, error) { + if g.isConstellationNotInUse { + return nil, nil + } + if len(data) == 0 { + return data, nil + } + // Ignore this error since not being a recipient of + // a payload isn't an error. + // TODO: Return an error if it's anything OTHER than + // 'you are not a recipient.' + dataStr := string(data) + x, found := g.c.Get(dataStr) + if found { + return x.([]byte), nil + } + pl, _ := g.node.ReceivePayload(data) + g.c.Set(dataStr, pl, cache.DefaultExpiration) + return pl, nil +} +``` +
+Constellation通过RPC调用Haskell的接口: +
+ +``` go +type Client struct { + httpClient *http.Client +} +func (c *Client) SendPayload(pl []byte, b64From string, b64To []string) ([]byte, error) { + buf := bytes.NewBuffer(pl) + req, err := http.NewRequest("POST", "http+unix://c/sendraw", buf) + if err != nil { + return nil, err + } + if b64From != "" { + req.Header.Set("c11n-from", b64From) + } + req.Header.Set("c11n-to", strings.Join(b64To, ",")) + req.Header.Set("Content-Type", "application/octet-stream") + res, err := c.httpClient.Do(req) + + if res != nil { + defer res.Body.Close() + } + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("Non-200 status code: %+v", res) + } + + return ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, res.Body)) +} +``` +
+通过上述的一系列调用,就产生一个RPC的请求,传递到Enclave. +
+3)、Enclave模块 +
+在Haskell的项目中主要有以下几个模块:主模块、Enclave模块、Node模块和相关的工具模块,这里重点介绍Enclave模块。 +
+ +``` haskell +--加密交易内容数据结构 +data EncryptedPayload = EncryptedPayload + { eplSender :: Box.PublicKey + , eplCt :: ByteString + , eplNonce :: SBox.Nonce + , eplRcptBoxes :: [ByteString] + , eplRcptNonce :: Box.Nonce + } deriving Eq +--实例化为Show类型 +instance Show EncryptedPayload where + show = show . encodeable + +--Hahkell语法,类似数据学函数语法,=号后即为函数体 +--交易内容的加密,包括下面的重载 +encodeable :: EncryptedPayload + -> (ByteString, ByteString, ByteString, [ByteString], ByteString) +encodeable EncryptedPayload{..} = + ( S.encode eplSender + , eplCt + , S.encode eplNonce + , eplRcptBoxes + , S.encode eplRcptNonce + ) +instance Binary EncryptedPayload where + put = put . encodeable + --后面以\表示lambda表达式 + get = get >>= \(sender, ct, nonce, rcptBoxes, rcptNonce) -> return EncryptedPayload + { eplSender = fromJust $ S.decode sender + , eplCt = ct + , eplNonce = fromJust $ S.decode nonce + , eplRcptBoxes = rcptBoxes + , eplRcptNonce = fromJust $ S.decode rcptNonce + } +encrypt :: ByteString + -> Box.PublicKey + -> Box.SecretKey + -> [Box.PublicKey] + -> IO EncryptedPayload --IO表示不纯函数,即非重入函数 +encrypt pl sender pk rcpts = encrypt' pl sender cks + where + cks = map (safeBeforeNM sender pk) rcpts + + +``` + +``` haskell +--重载函数 +encrypt' :: ByteString + -> Box.PublicKey + -> [Box.CombinedKey] + -> IO EncryptedPayload +encrypt' pl eplSender cks = do + (mk, eplNonce, eplCt) <- sboxSeal pl + eplRcptNonce <- Box.newNonce + let eplRcptBoxes = map (\ck -> Box.boxAfterNM ck eplRcptNonce emk) cks + emk = S.encode mk + return EncryptedPayload{..} +``` +
+相关的RPC: +
+ +``` haskell +--主线程启动监听网络 +defaultMain :: IO () +defaultMain = do + args <- getArgs + --通过配置文件来配置网线参数 + (cfg, _) <- extractConfig args + if cfgJustShowVersion cfg + then putStrLn ("Constellation Node " ++ version) + else do + _ <- case cfgWorkDir cfg of + Nothing -> return () + Just wd -> do + createDirectoryIfMissing True wd + setCurrentDirectory wd + case cfgJustGenerateKeys cfg of + [] -> withStderrLogging $ run cfg + ks -> mapM_ generateKeyPair ks +--启动并开户线程处理 +run :: Config -> IO () +run cfg@Config{..} = do + setupLogging cfgVerbosity + debugf' "Configuration: {}" [pShowNoColor cfg] + sanityCheckConfig cfg + setupParallelism + logf' "Constructing Enclave using keypairs {}" + [show $ zip cfgPublicKeys cfgPrivateKeys] + (crypt, pubs) <- setupCrypt cfgPublicKeys cfgPrivateKeys cfgPasswords + ast <- mustLoadPublicKeys cfgAlwaysSendTo + (selfPub, _) <- newKeyPair + logf' "Throwaway public key for self-sending: {}" [show selfPub] + storage <- setupStorage cfgStorage + (warpFunc, m, setSecure) <- setupTls cfg + nvar <- newTVarIO =<< + newNode crypt storage cfgUrl pubs ast selfPub cfgOtherNodes m + setSecure + --启动线程处理监听 + _ <- forkIO $ do + let mwl = if null cfgIpWhitelist + then Nothing + else Just $ whitelist cfgIpWhitelist + logf' "Public API listening on 0.0.0.0 port {} with whitelist: {}" + ( cfgPort + , Shown $ if isNothing mwl then ["Disabled"] else cfgIpWhitelist + ) + warpFunc (Warp.setPort cfgPort Warp.defaultSettings) $ + apiApp mwl Public nvar + _ <- case cfgSocket of + Just sockPath -> void $ forkIO $ runPrivateApi sockPath nvar + Nothing -> return () + registerAtExit $ do + log "Shutting down... (Interrupting this will cause the next startup to take longer)" + case cfgSocket of + Just sockPath -> resetSocket sockPath + Nothing -> return () + readTVarIO nvar >>= closeStorage . nodeStorage + log "Node started" + withAtExit $ runNode nvar +``` +
+API接口消息处理: +
+ +``` haskell +--request的接口处理 +request :: ApiType -> TVar Node -> Wai.Application +request apiType nvar req resp = do + b <- Wai.lazyRequestBody req + let h = Wai.requestHeaders req + path = Wai.pathInfo req + case parseRequest path b h of + Left err -> do + warnf "Failed to decode '{}' ({}) request: {}" + ( TE.decodeUtf8 $ Wai.rawPathInfo req + , TE.decodeUtf8 $ Wai.requestMethod req + , err + ) + resp badRequest + Right apiReq -> if authorizedRequest apiType apiReq + then do + eapiRes <- performRequest nvar apiReq + case eapiRes of + Left err -> do + warnf "Error performing API request: {}; {}" (Shown apiReq, err) + resp internalServerError + Right apiRes -> do + debugf "Request from {}: {}; Response: {}" + ( Shown $ Wai.remoteHost req + , Shown apiReq + , Shown apiRes + ) + resp $ ok $ response apiRes + else do + warnf "Blocked unauthorized request from {}: {}" + (Shown $ Wai.remoteHost req, Shown apiReq) + resp unauthorized + +parseRequest :: [Text] -> BL.ByteString -> RequestHeaders -> Either String ApiRequest +----- +-- Node client +----- +parseRequest ["send"] b _ = ApiSend <$> AE.eitherDecode' b +parseRequest ["receive"] b _ = ApiReceive <$> AE.eitherDecode' b +parseRequest ["sendraw"] b h = ApiSendRaw <$> decodeSendRaw b h +parseRequest ["receiveraw"] _ h = ApiReceiveRaw <$> decodeReceiveRaw h +parseRequest ["delete"] b _ = ApiDelete <$> AE.eitherDecode' b +``` + +
+逻辑节点的消息处理: +
+ +``` haskell +refreshLoop :: StdGen -> TVar Node -> IO () +refreshLoop g nvar = do + nodeRefresh nvar + -- Wait approximately five minutes + let (jitteredDelay, ng) = randomR (280, 320) g + threadDelay (seconds jitteredDelay) >> refreshLoop ng nvar + +nodeRefresh :: TVar Node -> IO () +nodeRefresh nvar = do + node <- readTVarIO nvar + let PartyInfo{..} = nodePi node + epis <- mapM + (getRemotePartyInfo nvar) + (HS.toList $ HS.delete piUrl piParties) + let (ls, rs) = partitionEithers epis + --符号‘$’的优先级最低的意义就是:它和附近的元素发生结合是最晚的。等价于其后跟() + forM_ ls $ \err -> warnf "Synchronization failed: {}" [err] + atomically $ mergePartyInfos nvar rs +--读取IO消息 +getRemotePartyInfo :: TVar Node -> Text -> IO (Either String PartyInfo) +getRemotePartyInfo nvar url = trys $ do + logf "Starting synchronization with {}" [url] + Node{..} <- atomically $ readTVar nvar + res <- simplePostLbs nodeManager nodeSetSecure + (T.unpack url ++ "partyinfo") (encode nodePi) + logf "Finished synchronization with {}" [url] + evaluate $ decode (responseBody res) +``` +
+haskell的代码纯数学的,理解起来有点头大啊,又回到从前的感觉。 +
+ +### 2、ZSL的分析 +
+在Quorum中,ZSL是定位于增强Constellation模块的。下面简要的分析一下ZSL的代码: +
+1)、在Quorum中增加的ZSL部分 +
+在1.6版本中,core,internal,params,eth中都增加ZSL的相关代码或者模块,在vendor的引用库中,也增加了相关的zsl的库的引用。 +
+其中主要的代码在core/zsl/api.go中,它主要引用了用GO封装的ZSL相关的接口,在代码中有一个注释说明: +
+ +``` +Example to generate a proof in geth: + +sk = "0xf0f0f0f00f0f0ffffffffff000000f0f0f0f0f00f0000f0f00f00f0f0f0f00ff" +pk = "0xe8e55f617b4b693083f883f70926dd5673fa434cefa3660828759947e2276348" +rho = "0xdedeffdddedeffdddedeffdddedeffdddedeffdddedeffdddedeffdddedeffdd" +value = 2378237 +zsl.createShielding(rho, pk, value); + +Results in: +cm = "0x58e38183982c6f7981e9f3ce0a735fdd4ca2f0cd88db6ee608c2fe1e84142d0d" +send_nf = "0xc813e257232fae0fee5244aadf98d7ab7a676724c128cd3c0d52d3a01739a3da" +ztoken.addCommitment(cm, {from:eth.accounts[0], gas:470000}) +rt = ztoken.root() +treeIndex = ztoken.getWitness(cm)[0] +authPath = ztoken.getWitness(cm)[1] +zsl.createUnshielding(rho, sk, value, treeIndex, authPath) +Verify with: +zsl.verifyUnshielding(proof, spend_nf, rt, value) +``` + +``` go +//生成接口实例 +func NewPublicZSLAPI() *PublicZSLAPI { + return &PublicZSLAPI{} +} +func (api *PublicZSLAPI) CreateShielding(rho common.Hash, pk common.Hash, value float64) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + snark.Init() + proof := snark.ProveShielding(rho, pk, uint64(value)) + send_nf := computeSendNullifier(rho[:]) + cm := computeCommitment(rho, pk, uint64(value)) + result["proof"] = "0x" + hex.EncodeToString(proof[:]) + result["cm"] = common.BytesToHash(cm) + result["send_nf"] = common.BytesToHash(send_nf) + return result, nil +} +func (api *PublicZSLAPI) CreateUnshielding(rho common.Hash, sk common.Hash, value float64, treeIndex float64, authPath []string) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + // copy authentication path array into two dimensional array (as required by snark.ProveUnshielding()) + if len(authPath) != ZSL_TREE_DEPTH { + return result, errors.New(fmt.Sprintf("Authentiction path must be %d in length", ZSL_TREE_DEPTH)) + } + var authenticationPath [ZSL_TREE_DEPTH][32]byte + for i := 0; i < ZSL_TREE_DEPTH; i++ { + b, err := hex.DecodeString(strings.TrimPrefix(authPath[i], "0x")) + if err != nil { + return result, err + } + var uncle [32]byte + copy(uncle[:], b[:32]) + authenticationPath[i] = uncle + } + //初始化snark库,这个是零知识证明的必须 + snark.Init() + proof := snark.ProveUnshielding(rho, sk, uint64(value), uint64(treeIndex), authenticationPath) + send_nf := computeSendNullifier(rho[:]) + spend_nf := computeSpendNullifier(rho[:], sk) + result["proof"] = "0x" + hex.EncodeToString(proof[:]) + result["send_nf"] = common.BytesToHash(send_nf) + result["spend_nf"] = common.BytesToHash(spend_nf) + return result, nil +} +func (api *PublicZSLAPI) VerifyShielding(proofHex string, send_nf common.Hash, cm common.Hash, value float64) (bool, error) { + + proof, err := getProofFromHex(proofHex) + if err != nil { + return false, err + } + + snark.Init() + result := snark.VerifyShielding(proof, send_nf, cm, uint64(value)) + return result, nil +} +``` +
+注意,这里只简要说明一下零知识证明,不详细阐述零知识证明的相关算法,如果对其有兴趣,可在网上查找相关的资料。 +
+零知识证明过程即参与的一方向另一方证明自己掌握某个秘密(或者说证明者向验证者证明自己掌握某个秘密)。在这个过程中,证明的一方并不想让另外一方知道这个秘密是什么。所以二者要按照一个约定,进行一系列的交互,然后最终由另外一方验证一方确实知道某个秘密。 +
+在计算机领域,一般做法是把原始问题映射到NP问题。验证者只要验证证明者给出的NP问题的解即可,这个计算量需求不大。Quorum是和zcash合作,在Zash中使用的NP问题是QAP(把向量表达式表示为多项式,从而把向量的验证转化为多项式的验证,这个过程称为QAP(Quadratic Arithmetic Programs)) +
+零知识证明的缺点,就在交互上,交互就意味着时间的延长,所以目前使用零知识证明的相关的程序,耗时都是一个瓶颈。 +
+其它的几个模块中,也增加了相应的接口说明和相关的模块,但基本都是给这个API接口做服务的。 +
+2)零知识证明的snark相关 +
+在Quorum中,使用Golang封装了相关的库。形成了一个单独的项目zsl-q(还有一个与其相关的zsl-q-params项目)。 +
+在封装的snark.go中: +
+ +``` go +func Init() { + onceInit.Do(func() { + C.zsl_initialize() + }) +} + +func VerifyTransfer(...) bool { + ret := C.zsl_verify_transfer(unsafe.Pointer(&proof[0]), + unsafe.Pointer(&anchor[0]), + unsafe.Pointer(&spend_nf_1[0]), + unsafe.Pointer(&spend_nf_2[0]), + unsafe.Pointer(&send_nf_1[0]), + unsafe.Pointer(&send_nf_2[0]), + unsafe.Pointer(&cm_1[0]), + unsafe.Pointer(&cm_2[0])) + +...... +} + +func ProveShielding(rho [32]byte, pk [32]byte, value uint64) [584]byte { + var proof_buf [584]byte +...... + C.zsl_prove_shielding(rho_ptr, pk_ptr, C.uint64_t(value), unsafe.Pointer(&proof_buf[0])) + +....... +} + +func VerifyShielding(proof [584]byte, send_nf [32]byte, cm [32]byte, value uint64) bool { + send_nf_ptr := C.CBytes(send_nf[:]) + cm_ptr := C.CBytes(cm[:]) + ret := C.zsl_verify_shielding(unsafe.Pointer(&proof[0]), send_nf_ptr, cm_ptr, C.uint64_t(value)) + +....... +} + +func VerifyUnshielding(proof [584]byte, spend_nf [32]byte, rt [32]byte, value uint64) bool { + ret := C.zsl_verify_unshielding(unsafe.Pointer(&proof[0]), + unsafe.Pointer(&spend_nf[0]), + unsafe.Pointer(&rt[0]), + C.uint64_t(value)) +...... +} +``` +
+这些代码和在Quorum中的Golang代码互相响应,但是真正的算法调用在下面: +
+ +``` c++ +void zsl_initialize() +{ + default_r1cs_ppzksnark_pp::init_public_params(); + inhibit_profiling_info = true; + inhibit_profiling_counters = true; +} + +bool zsl_verify_unshielding( + void *proof_ptr, + void *spend_nf_ptr, + void *rt_ptr, + uint64_t value +) +{ +...... + + auto witness_map = UnshieldingCircuit::witness_map( + std::vector(spend_nf, spend_nf+32), + std::vector(rt, rt+32), + value + ); + + r1cs_ppzksnark_verification_key verification_key; + loadFromFile("unshielding.vk", verification_key); + + if (!r1cs_ppzksnark_verifier_strong_IC(verification_key, witness_map, proof_obj)) { + return false; + } else { + return true; + } +} + +void zsl_prove_unshielding( + void *rho_ptr, + void *pk_ptr, + uint64_t value, + uint64_t tree_position, + void *authentication_path_ptr, + void *output_proof_ptr +) +{ +...... + + g.generate_r1cs_witness( + std::vector(rho, rho + 32), + std::vector(pk, pk + 32), + value, + tree_position, + auth_path + ); + pb.constraint_system.swap_AB_if_beneficial(); + assert(pb.is_satisfied()); + + r1cs_ppzksnark_proving_key proving_key; + loadFromFile("unshielding.pk", proving_key); + + auto proof = r1cs_ppzksnark_prover(proving_key, pb.primary_input(), pb.auxiliary_input(), pb.constraint_system); + +...... +} +``` +
+在向下的代码,就是零知识证明的c++实现和库了,这里就不再分析,有兴趣的可以将源码下来,学习一下。 +
+
+ +# 五、总结 +
+通过上述的分析,可以看到,Quorum把privacy独立出来,确实想法比较不错,既可以依赖以太坊的公网技术实现区块链的功能,又可以在其基础上开发相应的专门的联盟链的分支,达到自己的目的。 +
+不管是从设计上还是技术的实现上,Quorum力求简单明了,在整个源码的阅读过程中,看不到什么特别的设计技巧,显得非常的接地气。 +
diff --git a/roadmap.pdf b/roadmap.pdf new file mode 100644 index 0000000..3766217 Binary files /dev/null and b/roadmap.pdf differ diff --git "a/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\344\273\213\347\273\215.md" "b/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\344\273\213\347\273\215.md" new file mode 100644 index 0000000..18985e0 --- /dev/null +++ "b/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\344\273\213\347\273\215.md" @@ -0,0 +1,59 @@ +# RSA累加器的介绍 + +## 一、RSA累加器的介绍 +一个密码学上的累加器是一个单向的隶属函数。它可用于识别一个候选是否为一个集合的成员,且不会在过程中暴露集合中的成员。这是WIKI上的定义。 +
+目前在主流的区块链中,用来对交易进行验证的是通过默克尔树传递数据来进行的,特别是轻量级节点和钱包,基本是离不开默克尔树的数据交换的。如果在比特币网络中,还稍好一些,毕竟比特币的交易速度有限。但随着以太坊及EOS的出现,交易量开始不断上升,那么,做为需要进行网络通信的默克尔树的数据,就开始变得越来越大。 +
+所网上资料讲,V神估计以目前的Plasma子链数据交易状况,默克尔树的数据大小应该在2.5G左右,这不是一个小的数据,它使得存储和数据交换的压力会越来越大。同时,V神提出如果使用RSA ACCumulactors累加器的方式来替代默克树的话,可以将上述的数据降低到大约大3.6M左右,这是一个非常大的提高。 +累加器:“一个密码学累加器,其会产生对一组元素的短期约束承诺,以及对集合中任何元素的短期成员身份和非成员身份证明。” +
+动态累加器:“支持添加和删除具有O(1) 成本的元素累加器,其与累积元素数量无关。” +
+通用累加器:“支持成员和非成员身份证明的动态累加器。” +
+批处理:批验证n个证明,要比验证单个证明要快n倍。 +
+聚合:在一个常量大小的证明中聚合n个成员证明。 +
+未知顺序组:组的顺序是其集合中元素的数目。为了保证所提供的证明的安全性,需要生成一组未知顺序(否则累加器中使用的模数有已知的因子分解,并且可以创建伪证明)。生成它可通过多方计算完成,但如果生成方串通检索生成的数的阶乘,则这是不安全的。它可通过使用类组在没有可信设置的情况下生成(注:这点是非常重要的) + +## 二、累加器涉及到的知识 + +1、双线性映射和双线性函数 +也即双线性配对,一个双线性映射是由两个向量空间上的元素,生成第三个向量空间上一个元素之函数,并且该函数对每个参数都是线性的。线性的意思就是可加与成比例。 +RAS累加器和双线性累加器都是密码学中的方法。累加器多用于环签名中。双线性映射就会引出双线性函数,举一个例子,二次型就是一个对称的双线性函数。双线性映射有三个特点:双线性性,非退化性和可计算性。 +
+2、离散对数 +在整数中,离散对数(英语:Discrete logarithm)是一种基于同余运算和原根的一种对数运算。而在实数中对数的定义 logba是指对于给定的a和b,有一个数x,使得bx=a。相同地在任何群G中可为所有整数k定义一个幂数为bx,而离散对数logba是指使得bx=a的整数k。 +
+3、互质 +又称为互素,在数论中,如果两个或两个以上的整数的最大公约数为1,则称他们为互质。 +
+4、奇质数 +质数(Prime number),又称素数,指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个正因数的数)。大于1的自然数若不是素数,则称之为合数(也称为合成数)。奇质数,系指任何大于2的素数。 +
+搞加密对数学的要求还是有一些的,比较头痛。 + + +## 三、整体的流程 +这里把基本的流程过一下,实际的算法可能会比这个复杂不少,但原理是这样的: +## 1、给定一个模数N和一个发生器G(代表空的累加器),对于集合{u}和增加到累加器集合的U,设定累加器C,(C,N都很大),则有: +C = G ^ U%N + +## 2、从{u}中获取值的子集{r}。为了计算证明,上需要{u}的所有其他值,将那些秘密值标记为{s}。即有R * S = U.证明P是: +P = G^S % N + +## 3、向证明方提供{r}和P. 计算C'并验证它等于C: +C'= P ^ R%N +## 4、通过替换P,可以看到C'必须等于C: +C'= G ^ S ^ R = G ^(S * R)= G ^ U = C%N + +上面的说法比较抽象,举一个简单的例子: +假设要把3,5,11这几个值增加到累加器c中,那么C=G^165,为证明3是累加器的一部分,可以使用(G^3)^x=C来证明它。在这种情况下x=55,在实际的应用中,肯定不能这么简单,一般来说会把指数做得非常大。这就需要上面的代数式来进行推算证明。 +
+为了安全起见,可以在{u}中添加一个很大质数,这在加密算法里叫做加盐。 +上面是证明成员身份的过程,而证明非成员的过程比较复杂,这里推荐大家看一下相关的文章,V神也提出了自己的证明方法,在后面分析到相关的部分时,再进行具体的说明。 + +## 四、总结 +这里仅仅是简单的对RSA ACC进行了一个简单的介绍,重点是明白累加器到底是什么。累加器在加密领域的应用从1994年开始,在区块链中的应用,没有多长时间,累加器的优点在上面提到了,缺点也很明显,目前来看,累加器的验证集合是不断的动态变化的,导致证明的过程有些复杂,网上有相关的例子,可以从GITHUB上下来运行一下,随着数据的增长,还是相当慢的,距离实用仍然还要走一大段路。 diff --git "a/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\350\256\272\346\226\207\347\256\200\350\246\201\350\257\264\346\230\216.md" "b/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\350\256\272\346\226\207\347\256\200\350\246\201\350\257\264\346\230\216.md" new file mode 100644 index 0000000..37c3119 --- /dev/null +++ "b/rsa-acc/RSA\347\264\257\345\212\240\345\231\250\347\232\204\350\256\272\346\226\207\347\256\200\350\246\201\350\257\264\346\230\216.md" @@ -0,0 +1,95 @@ +# RSA累加器的论文简要说明 + +## 一、目前对RSA累加器的相关论文 +目前RSA累加器的论文有好多,前面也介绍过,从1994年,RSA相关的研究就展开了。这里只介绍一篇斯坦福大学的针对区块链的默克尔树的相关的这方面的论文,即《Batching Techniques for Accumulators with Applications to IOPs and Stateless Blockchains》,中文《用于IOP和无状态区块链的累加器批处理技术》。这篇论文的地址在: +
+https://eprint.iacr.org/eprint-bin/getfile.pl?entry=2018/1188&version=20190314:012023&file=1188.pdf +
+需要说明的是,这篇论文的的版本有好几个,更新的频率还是挺快的,大家需要根据实际情况来分析。 + +## 二、概要 +在这篇论文中主要介绍了以下几个方面: +#### 1、提出一个在去中心化的场景下使用的批处理的累加器。 +#### 2、使用非交互的方式以恒定大小进行成员和非成员的证明。 +#### 3、未知顺序组的证明(求幂和零知识证明及两组元素间离散对数知识证明) +#### 4、无状态区块的构造和批处理。 + +## 三、内容分析 +先说明一下,可能因为个人的能力和数学水平,有些知识点不一定准确,如果有疑问,请大家参考上面的论文。 +下面就分段进行分析: +## 1、介绍 +这部分主要是介绍了累加器的定义和静态动态累加器。同时以比特币为一个例子,通过UTXO来构建交互式 oracle 证明 (IOPs)和概率可检证明 (PCPs)。通过扩展RSA累加器来进行批量的成员和非成员证明。其实这就类似于默克尔树的作用了。 +## 2、前提条件 +论文中给定了六个定义:自适应假设,强RSA假设,完整性,论点(知识),零知识和非交互式参数。 +这一部分的比较麻烦,好多的数学理论,需要一定的数学功底。这里不展开说了,有兴趣仔细看看论文。 + +## 3、隐序组的简洁证明 +这部分分成了以下几个证明方式: +求幂证明(POE): +![poe](img/poe.png) + +同态原像的简洁证明: +
+ +![pohp](img/pohp.png) +
+离散对数知识证明(POKE): +
+ +![poke](img/poke.png) +
+同态原像知识的简洁证明: +
+ +![pokhp](img/pokhp.png) +
+离散对数的简洁零知识证明: +
+ +![zkpokerep](img/zkpokrep.png) +
+ +![zkpoke](img/zkpoke.png) +
+Aggregating Knowledge of Co-prime Roots(翻译不好): +
+ +![pokcr](img/pokcr.png) +
+ +![pokcrrep](img/pokcr-multiexp.png) +## 4、通用累加器 +这节主要是讲通用累加器的构建: +
+ +![base](img/base-rsa.png) +
+累加器的构建步骤在论文上有详细的描述,在4.1节: +
+ +![dy](img/rsa-dy.png) +
+下面是非交互式的证明: +
+ +![build](img/acc-build.png) +在累加器的构造过程中,如果使用非交互式的方法可以大幅提高效率,正如在零知识证明之中一样,在累加器的 +## 5、少量参数批向量承诺 +这节主要利用批处理来进行类似于默克尔树的验证方案: +
+ +![transfer](img/transfer.png) +使用bezout是用来进行非成员函数证明的。通过增加NI-POKE2来确保安全性。 +## 6、哈希素数 +在前面提到过质数的相关定义,在累加器中,为了安全起见,需要处理累积的值来判断是否为质数,抵抗协同因子的攻击。所以在在本文中通过一系列的防碰撞的哈希函数的简单算法,在素数域内处理累加器的计数。 +
+同样,在累加器的创建和使用过程中,对相关的数据也要求处理成素数。 + +## 7、应用程序 +这部分主要是进行一个无状态的区块链的创建,利用累加器实现高效的UTXO承诺,允许用用户在非完整的UTXO集合时进行交易。为了达到这个目的,为RSA累加器的批处理技术创建了新的简洁的零知识证明来处理未知顺序的组的证明。 + +## 8、安全证明 +此部分是提供上应用到的证明过程,可以忽略。 + +## 四、总结 +这篇文章主要是抛砖引玉,毕竟对数学加密这一块还是初学者,希望能通过这篇短文和大家一起探讨累加器在区块链上的应用场景。 diff --git a/rsa-acc/img/acc-build.png b/rsa-acc/img/acc-build.png new file mode 100644 index 0000000..1902486 Binary files /dev/null and b/rsa-acc/img/acc-build.png differ diff --git a/rsa-acc/img/base-rsa.png b/rsa-acc/img/base-rsa.png new file mode 100644 index 0000000..4f4d5f1 Binary files /dev/null and b/rsa-acc/img/base-rsa.png differ diff --git a/rsa-acc/img/pairing-algo1.png b/rsa-acc/img/pairing-algo1.png new file mode 100644 index 0000000..ca43ef7 Binary files /dev/null and b/rsa-acc/img/pairing-algo1.png differ diff --git a/rsa-acc/img/poe.png b/rsa-acc/img/poe.png new file mode 100644 index 0000000..1913300 Binary files /dev/null and b/rsa-acc/img/poe.png differ diff --git a/rsa-acc/img/pohp.png b/rsa-acc/img/pohp.png new file mode 100644 index 0000000..3ab9156 Binary files /dev/null and b/rsa-acc/img/pohp.png differ diff --git a/rsa-acc/img/pokcr-multiexp.png b/rsa-acc/img/pokcr-multiexp.png new file mode 100644 index 0000000..9b05bac Binary files /dev/null and b/rsa-acc/img/pokcr-multiexp.png differ diff --git a/rsa-acc/img/pokcr.png b/rsa-acc/img/pokcr.png new file mode 100644 index 0000000..d6ac79c Binary files /dev/null and b/rsa-acc/img/pokcr.png differ diff --git a/rsa-acc/img/poke.png b/rsa-acc/img/poke.png new file mode 100644 index 0000000..228fdf5 Binary files /dev/null and b/rsa-acc/img/poke.png differ diff --git a/rsa-acc/img/pokhp.png b/rsa-acc/img/pokhp.png new file mode 100644 index 0000000..6072b82 Binary files /dev/null and b/rsa-acc/img/pokhp.png differ diff --git a/rsa-acc/img/pokrep.png b/rsa-acc/img/pokrep.png new file mode 100644 index 0000000..71b8fd9 Binary files /dev/null and b/rsa-acc/img/pokrep.png differ diff --git a/rsa-acc/img/rsa-dy.png b/rsa-acc/img/rsa-dy.png new file mode 100644 index 0000000..4faf641 Binary files /dev/null and b/rsa-acc/img/rsa-dy.png differ diff --git a/rsa-acc/img/transfer.png b/rsa-acc/img/transfer.png new file mode 100644 index 0000000..715510d Binary files /dev/null and b/rsa-acc/img/transfer.png differ diff --git a/rsa-acc/img/zkpoke.png b/rsa-acc/img/zkpoke.png new file mode 100644 index 0000000..a14df29 Binary files /dev/null and b/rsa-acc/img/zkpoke.png differ diff --git a/rsa-acc/img/zkpokrep.png b/rsa-acc/img/zkpokrep.png new file mode 100644 index 0000000..3a05a58 Binary files /dev/null and b/rsa-acc/img/zkpokrep.png differ diff --git "a/rsa-acc/pairing\347\232\204\345\210\235\346\255\245.md" "b/rsa-acc/pairing\347\232\204\345\210\235\346\255\245.md" new file mode 100644 index 0000000..202f3f0 --- /dev/null +++ "b/rsa-acc/pairing\347\232\204\345\210\235\346\255\245.md" @@ -0,0 +1,38 @@ +# pairing的初步 + +## 一、ECC pairing的介绍 +在前面提到了RSA ACC累加器,它的缺点是需要不断的重复进行运算证明,速度还是是一个硬伤。这时就有人提出了Pairing:二元映射。特别是基于ECC Pairing,效率更好。 +
+不过在刚刚提出这个算法时,e(P,Q)的计算量也是巨大的,所以不言而喻。但随着谷歌等一些公司的投入和大牛们的不断的努力,运算的时间已经从几分钟缩减到微秒级。 +
+2008年,Vercauteren提出了最优Ate Pairing,这个是目前知道的最快的算法。 + +## 二、标识密码体系(IBC)和应用 +在传统的PKI加密体系中,需要公开一个公钥和自己保密一个私钥。但是在传送公钥时,很有可能出现问题,此时提出了CA认证。但是这玩意儿一个是太复杂,另外需要额外维护一个中心化的节点,一但出问题也就有问题了。 +
+为了解决这个问题,以色列密码学家在1984年提出了标识密码体系。即不再使用上述的方式来生成公私钥对,而是由客户标识如:姓名、IP、手机号以及邮箱等来作为公钥。私钥则由密钥中心依据系统主密钥和用户标识计算得出。直到2000年,业界提出基于椭圆曲线数学难题的配对构造算法(即上面第一节的介绍),才解决了这个根本的问题。 +
+在我国,2008年将其引入到国密的SM9中,目前在这方面的研究工作还是有很多人在推进。网上可以找到N多SM9的实现的算法,包括各个科研机构中开源的相关c++的源码。 + +## 三、主要原理 +IBC密码体系主要有三块: +
+1)IBE加解密算法组: +
+其基本的步骤为:生成系统参数和主密钥并保管好,其中系统参数是公开的;然后利用这二者和用户身份标识生成私钥,当然,标识即为公钥;利用系统和公钥对相关数据进行加密,得到密文;然后相关方利用系统参数和私钥解密。 + +2)IBS签名算法组: +
+签名类似,不同点在于用私钥和系统参数产生签名,然后用此签名和系统参数及公钥验证。原理和普通的PKI体系没有太大不同。 + +3)IBKA身份认证协议: +
+认证的协议首先由发起方生成一个随机数和时间戳,发送给认证方;认证方通过随机数和时间戳生成待签报文,用私钥生成数字签名发回给发起方;发起方利用认证方的公钥对数字签名进行验证。 +
+IBC的优势在于运算数据小,成本低,不需要第三方,但是目前看来这个技术还在发展中,不能说其可以完全替代PIK体系。 + +## 四、区块链上的应用 +在区块链上的应用也和RSA ACC类似,正处于研究阶段,也是用来对相关的验证数据进行替代,目前真正商用的还没有看到,研究的倒是出现了一些。 + +## 五、总结 +加密体系是一个比较深的东西,对一些原理和算法的过程理解需要不断的完善,欢迎指正。 diff --git "a/rsa-acc/\344\270\200\344\270\252pairing\345\272\223\347\232\204\344\273\213\347\273\215.md" "b/rsa-acc/\344\270\200\344\270\252pairing\345\272\223\347\232\204\344\273\213\347\273\215.md" new file mode 100644 index 0000000..d3337cb --- /dev/null +++ "b/rsa-acc/\344\270\200\344\270\252pairing\345\272\223\347\232\204\344\273\213\347\273\215.md" @@ -0,0 +1,37 @@ +# 一个pairing库的介绍 + +## 一、论文及相关库的介绍 +这是一篇改进BN曲线算法的论文,相关论文《High-Speed Software Implementation of the Optimal Ate Pairing over Barreto–Naehrig Curves》的地址: +https://eprint.iacr.org/2010/354.pdf +它原来提供的源码的地址已经经失效,但是在Github上可以找到这个源码库地址: +https://github.com/herumi/ate-pairing +这个库是用c++实现的,一般来看,算法这一块的源码用c/c++实现的还是比较多的。毕竟都在抢效率。 + +## 二、特点 +这篇论文及开源的代码主要是针对X86-64平台设计的Optimal Ate Pairing,所以可能在嵌入式的应用上不妥。其采用通过的蒙哥马利素域乘法器精心实现了基域的各种算法,效率相当高。在i7 2.8G的单核处理器上实现了消耗233万时钟周期(0.832毫秒)计算出254bit素数域Fp上的Optimal AtePairing。 +
+其实最大的特点是有开源代码啊,这省了国人多少事。 + +## 三、相关内容 +## 1、首先介绍了传统的双线性配对。 +## 2、对Barreto-Naehrig曲线的最佳配对进行了说明: +follows [5]: +p(t) = 36t4 + 36t3 + 24t2 + 6t + 1, +r(t) = 36t4 + 36t3 + 18t2 + 6t + 1, (1) +tr(t) = 6t2 + 1, +其中 t∈Z是任意整数,使得p = p ( t )和r = r ( t )都是 质数。 另外, t必须足够大以保证足够 安全级别。 + +## 3、具体的算法步骤 + +![algo1](img/pairing-algo1.png) + +## 4、对算法的说明 +主要是Tower Extension Field Arithmetic从两个方面阐述,Computational Costs of the Tower Extension Field Arithmetic、Frobenius Operator,具体的说明参看论文。 + +## 5、对Optimal Ate Pairing的成本计算 +这里面介绍了米勒循环等,重点分析了算法的计算方式和步骤中对计算的要求量。 +## 6、得出结果 +通过上面的过程,在前面提到的机器配置上得到了结果,不过这个不具有普遍意义还需要进一步的完善和修改。 + +## 四、总结 +这个论文是专门针对PC设计的,所以效率还是相当高的。但是具体的一些细节,需要大家认真看论文和相关的代码。 diff --git a/scalability/.gitkeep b/scalability/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scalability/Chain_Interoperability.md b/scalability/Chain_Interoperability.md new file mode 100644 index 0000000..d76c122 --- /dev/null +++ b/scalability/Chain_Interoperability.md @@ -0,0 +1,27 @@ +跨链交易难点在于保证交易的原子性,比如ETH兑换BTC其实至少发生俩笔交易,分别发生在ETH和BTC上, 必须俩笔交易要么同时成功, 要么一笔失败的时候俩笔交易必须同时失效, 具体到实现就需要 1 如何获取另一条链上的消息 , 2 怎么保证消息可靠, 3 如何应对在另一条链上期待的转账 ++ _针对1、2:1>通过可信的中介结构收听对方链上的消息, 2>通过在本链上维护一个对方链的轻节点作为可信的消息源处理其他合约验证请求_ ++ _针对3: 通过deposit一笔锁定资金到某个合约,1> 由中介决定是否放行, 2> 由合约判断对方提交的证据判断是否放行_ + +针对以上的三个问题决绝方案不同,市面上流行的跨链解决法案有:中介、中继, hash locking; 中继的典型代表有BTC-repy、cosmos-hub, 中介的典型代表有撮合交易, hash locking 典型的有雷电网络, 撮合交易系统不再赘述。 +1. BTC Replay的实现分为三个部分: (假设 Alice 用eth 购买 Bob手里的btc) ++ _将BTC的header搬到eth的合约上, 通过该合约可以视为是维持了一个btc header 的轻节点, 它可以使用SPV验证某个tx 是否发生了,为了鼓励大家提供btc的header, 可以允许给提供header信息的节点设置服务费_ ++ _Alice deposit eth到某个负责转账的contract, 该contrct 的作用允许任何人提供验证btc转账发生该节点负责调用eth 上的btc 轻节点合约 验证tx的有效性然后 然后通过解析btc上的tx来判断某个addr 是否收到了足够的BTC, 验证通过给Bob的eth 地址转账_ ++ _Bob 发现Alice deposit足够的eth到转账合约后, 给Alice在BTC上的地址上转账_ + +**_同样我们可以将这一模式搬到任何支持智能合约的公链X上_** + +2. cosmos 其实由tendermint驱动的一个公链架构的方案, 它不是一条具体的链, tendermint 是一个pos + pbft 驱动的可插拔公链的基础架构,tx 的验证、block 的apply 通过调用RPC执行, p2p作为 tendermint 的入口, 其他部分比如 consensus、mempool、blockchain等通过向p2p注册reactor接口的方式驱动, 各个模块间通过event hub的相互驱动, +tendermint 进一步修改了经典pbft的算法, proposer 通过节点在pos中的 despoit 的数量设置可发起提案的权重, 通过权重计算下一轮的proposer写入本次提案中, 下一轮发起是验证节点先判断自己是否有资格发起提案, 将经典的三步 完成commit, 演化成俩步完成。 在RPC调用block apply 的时候会更新快上的验证节点集合。 +cosmos 做跨链的转账的解决方案如下( 假设 Alice 用eth 购买 Bob手里的btc) ++ _创建一条称之为Hub(实现cosmos方案的公链)的公链, 通过 hub字面义可以看出这一条消息负责消息分发的公链_ ++ _在ETH创建一条侧链(我们称之为 Z1), 在ETH上部署一个合约负责押金接受、放行等,这条侧链可以用tendermint驱动的也可以方案实现的,该侧链的作用1> 跟踪该contract上发生的交易, 对交易签名后发给HUB, 2>负责对发出的消息验证, 3> 接受可信的消息后调用contract的给Bob转账_ ++ _在receiver 所在的公链同样的创建一条类似于z1的公链Z2,作用与2相同_ + + +3. [雷电网络](https://github.com/XChainLab/documentation/tree/master/scalability/raiden) + + + + + + diff --git a/scalability/README.md b/scalability/README.md new file mode 100644 index 0000000..d844746 --- /dev/null +++ b/scalability/README.md @@ -0,0 +1,2 @@ + +- [raiden-network](raiden) diff --git a/scalability/plasma/imgs/new-architecture.png b/scalability/plasma/imgs/new-architecture.png new file mode 100644 index 0000000..85551fb Binary files /dev/null and b/scalability/plasma/imgs/new-architecture.png differ diff --git a/scalability/plasma/imgs/work-follow.png b/scalability/plasma/imgs/work-follow.png new file mode 100644 index 0000000..69fed28 Binary files /dev/null and b/scalability/plasma/imgs/work-follow.png differ diff --git a/scalability/plasma/plasma-in-10-mins.md b/scalability/plasma/plasma-in-10-mins.md new file mode 100644 index 0000000..aa4b334 --- /dev/null +++ b/scalability/plasma/plasma-in-10-mins.md @@ -0,0 +1,51 @@ +# 10分钟介绍Plasma +今天 Vitalik Buterin 和 Joseph Poon 发布了一个关于[Plasma项目](http://plasma.io/)的工作计划。 + +让我来解释下这个项目是如何工作的。我希望这会给你节约一点时间。 + +以太坊目前已经有一个Lightning Network(闪电网络)应用(还未发布为产品)叫做[Raiden(雷电网络)](https://github.com/raiden-network/raiden) + +> 其基本思想是两种模式的转换,从所有的交易都放到区块链上的共享账本(这是瓶颈)的模型,切换到用户可以私下交换转账消息的模型。 +> +> Raiden 使用点对点支付(P2P pay)通道和保证金的方式,实现了区块链一样的安全保证。 +> +> Raiden 是以太坊的一个扩展的实施方案。一个 Raiden 节点同以太坊的节点一起运行,当 Raiden 节点与其他 Raiden 节点进行通信时,用来完成转账;当 Raiden 节点与以太坊节点进行通信时,用来管理保证金。 + +即使你在链下发送数百万交易,你的 gas 费用还是会非常的低,因为你只需要少量的燃料费用来确保上链成功(例如,你可以在24小时内发送一笔交易到链上)。 + +欢迎Plasma加入到以太坊架构 + +![](imgs/new-architecture.png) + +## 区块链中的区块链 +同 Lightning Network 相似,Plasma 是由一系列运行在主区块链(译者:root blockchain)(即Mainnet 以太坊区块链)上的合约。主网络合约运行只需要处理少量子区块链的交易请求,这些交易请求能够执行令人难以置信的大量计算。交易请求会定期从子区块链上向主区块链上进行广播。人们可以将主区块链视为最高法院,其他所有下级法院都从中获取权力。 + +然而,由于并非所有数据都被传播给所有人(只会传播那些希望得到验证状态的人被指定特殊状态的人),所以大家只会负责去定期监控自己指定的感兴趣的特定链,用来防止欺诈等行为。当发生攻击时,参与者能快速的用低廉的代价,将大部分撤回到主链中。 + +区块链可以分层排列在树形结构中。这将会使得它创造较好的平衡系统,以最大的限度提高数据可用性、安全性并降低成本。完全安全的挖矿工作只会在主链上完成,只有主链才具有安全性和证明的工作。 + +额外的扩展性来自于消除了那些不受经济条件影响的链,而专注于会执行正确行为的链。 + +_目前有很多系统,像 Cosmos_Polkadot 等都采用了分层的区块链。/ + +每一条子链都用自己的token(就像是加密货币支付给矿工的奖励一样),用来激励这条链的验证器继续运行,并提供保护,防止出现由作弊证明(Fraud Proofs)规则检查出错误状态。一旦发现作弊(“扣留攻击”或者“数据可用性问题”)-> 作弊的验证者将会被扣除存款(即债务)。 + +![](imgs/work-follow.png) + +## 欺诈证明 +假设区块数据是有效的,每个子链的所有状态都会通过欺诈证明(智能合约的逻辑)来执行,欺诈证明允许任何执行者执行无效的区块。欺诈证明需要确定所有状态转换都是验证过的。另外,这些欺诈证明还执行了一个资金提现的互动协议。就像 Lightning Network 提款的时候需要过一段时间才能完成。 + +## 小结 +Plasma是区块链众多可扩展性解决方案之一(分片、PoS、LN等)。我希望它能够尽快发布。一些公司已经宣布他们将使用Plasma —— [OmiseGo](https://twitter.com/omise_go) + +原文:https://medium.com/chain-cloud-company-blog/plasma-in-10-minutes-c856da94e339 + + + + + + + +#翻译/Plasma + + diff --git a/scalability/raiden/README.md b/scalability/raiden/README.md new file mode 100644 index 0000000..dc17b87 --- /dev/null +++ b/scalability/raiden/README.md @@ -0,0 +1,254 @@ +**_先给出大白话的总结:_** 试想ABCD四个小伙伴做游戏,游戏中有一步是资金转账,通常情况下他们之间的转账都需要去银行排队、填单子转。为了方便**快捷**也出于**可以信任**的原因,他们之间的转账都通过打白条的方式,这有个前提是他们得都知道其他人的可用资金是多少(换句话说就是不能让对方多花了, 别人合理的白条谁都得承认),白条也是可用的资金,比如A给B打了个10元的白条, 那么A就知道B比刚才多了10元, B若要给A转账的话, A处认可的B的可用资金会比开始时多了10元,但是这就造成了几个潜在的问题,1:随着时间的推移小伙伴之间无法准确的知道对方的剩余可用资金, 因为资金流向是多方向的,2:这些白条怎么兑现,3:有人造假怎么办 ++ __**针对1:**__ 让资金单向流动, 我们规定所有的小伙伴之间俩俩配对, (A,B)(B,C)(C,D)就是一种可能的组合, 转账只能在配对的组合内或者通过对内的小伙伴转给外部小伙伴, 这样就保证了, 小伙伴的剩余可用资金对于对内的伙伴永远是可查的, 但是这也带来了一个问题跨对转账怎么完成例如A给D转账, 解决方案是我们规定(A,B)是一条边, 那么A需要找到一条A到D的通路, 转账就发生在这条通路上,所以此时A知道 A->B ->C ->D 是一条通路, 另外我们也规定这样的路上的转账的资金必须被锁定(意思是组内收到白条的一方的可用资金不会增加,但是打白条的一方的可用资金会减少), 那么A就给B打个锁定资金的白条, 白条是有附带信息“知道了secret才能解锁资金, 计算方法是hash(secret, lockroot')==locksroot, 其中lockroot'是上一笔该小组内的处于锁定状态白条的的lockroot ”才能生效, B也照此给C打个白条, C同样如此给D打白条, D收到白条后告诉A, 他收到锁定白条了, 那么此时A就把这笔解锁这笔资金的secret告诉D, 但是此时资金还是处于锁定状态,D的可用资金没有增加(是否增加取决组内的C是否承认), 因为知道了secret是可以强制解锁资金但是这么操作会有惩罚, 那么他会立刻告诉C他收到了secret, C就会重新打个条子并添加额外的信息“为友谊的小窗不翻,这个白条是用来抵消刚才的锁定的那笔资金, 同时也表示那笔处于锁定状态的白条无效了”, 如此重复直到A处。 细心的同学可能发现B出现在俩个组内就意味B的可用资金double了, 事情肯定不这么简单, 我们规定这种情况下B的资金必须拆分为俩份。 ++ **__针对2:__** 我们强制规定1: 必须和其他小伙伴组队, 2:组队的同时必须交一笔押金, 所以开始时可用资金就等于押金,3:增加一个裁判的角色,押金交到裁判这里, 裁判负责仲裁并负责兑现白条(contract充当这个角色) ++ **__针对3:__** 每个白条必须有自己签名, 裁判只认签名然后按规矩办事(私钥签名) + +**普通白条的定义 :** + +```python +{ + transfer_amount + n, //(总计转出去了多少钱+现在转多少, 注意不是现在要转多少), + nonce,//(第几次转账), + locksroot,//(处于锁定状态的资金有哪些), + signature, + hash(transfer_amount, nonce, locksroot), +} +``` + +**这么定义的好处** +1. transfer_amount 1:接收方只需要保存最后一个白条即可, 2: 防止重放攻击,裁判只给你判决一次(所以你只会拿着最后一张白条去提现) +2. locksroot:1: 防止通道上锁定资金在收到普通的白条后强制解锁资金, 因为解锁白条会将锁定白条设置无效, 具体操作就是在打条子的时候把locksroot设置为锁定白条中的locksroot‘, 如此对方是没有机会提现该笔被解锁的资金,因为merkel tree 不包含它了 + +**锁定资金的白条** + +```python +{ + transfer_amount, // 总计转出去了多少钱, 不是现在转多少 + nonce, + expiry, // 过期时间,小于某个blocknum之前这个白条有效 + hash(amount, nonce, locksroot'), + locksroot, // locksroot = hash(locksroot‘, secret, expiry, n(现在要转多少)), locksroot‘ 表示上一笔锁定的锁定资金白条中的,找裁判兑现这个白条必须要知道secret才可以 + signature, +} +``` + +### **具体的问题** +1. **怎么提现?** +_组内成员每人一次提交普通白条的机会, 提交锁定白条的次数不受限制, 但是你得按照 merkel proof姿势去解锁每一笔锁定的白条_ +2. **怎么找到小伙伴?** +_可以随机加入, 可以自由组合, 但是一旦组成立了, 游戏内的其他小伙伴会发来贺电, 因为他们可能会借用你们的通道_ +3. **有什么瓶颈, 怎么解决?** +_跨组转账的话, 你得通过别人的通道并且借用别人的资金池, 所以找到一条合适的通道就需要俩个条件 1:有一条可到达的路, 2: 路上的小伙伴之间的认可可用资金大于你要转的资金, 这会造成热点故障,原因是1: 本来加入多个组的话资金就会分散, 没有多少人愿意这么干, 但是先要找到这样的通路, 必须得有人自愿加入多个组, 2: 处于锁定状态的资金是不会被对方承认的,热点节点的资金压力大, 3:流量压力; 解决办法给愿意承担桥接转账的节点一定的手续费 +4. **为何要锁定资金?** +_因为跨组转账的不确定因素太多, 可能中途发现没有通路了, 可能有节点作恶、它收到中间转账但是不往下传,将转账资金锁定就确保了在target节点没有确认到帐前中间转账过程中资金不会丢失, 不好影响也是很显然的,就是造成别的节点的资金压力和流量压力_ +5. **怎么兑现锁定白条?** 因为所有锁定白条会形成一个merkel tree, 而叶子节点包含(n(锁定了多少钱, expiry, secrethash)),同时我们要求先提交普通白条,因为普通白条中包含最后的merkel tree root, 所以提现的时候拿着锁定资金的基本信息和这笔资金在这个merkel tree上的叔叔节点、叔爷爷节点...组成的list, 裁判计算 merkel tree root == hash(hash(hash(基本信息), 叔叔节点), 叔爷爷节点) ...)一致后, 在给提供证据的一方账户加n元钱, 如此类推解锁每笔锁定资金 +6. **不能一直等待** +_因为网络或者其他原因,不能让locked的白条一直占用资金,所以在在打locked白条的时候价格过期时间,大于某个blocknum后这个条子就没有用了, 所以在类似A给D转账的时候B给C的locked白条必须小于A给B的, 如此类推。 + + +### 实现的原理 +1. 请先阅读 [What is the Raiden Network?](https://raiden.network/101.html) +2. ##### 合约的类图 +
+ +![plugin-pic](imgs/smart_contract_obj.png) + +
+ +### 优化建议 ++ channel状态的可以支持使用双方签名的状态修改操作, 比如提现操作channel内的双反都签名后就直接转账、关闭通道即可 ++ 注册在 EndpointRegisty 节点可以作为p2p 的种子节点, 如此routing可以采用kademlia算法 + +### 基本架构 +雷电网络四个服务构成 +1. blochchain service : 用于和blockchain 交互, 例如获取 netting-channel-info, node的(host, port)等信息, 发送transaction(例如withdraw, close, deposit等), blockchain 主要由四部分构成 eth_client(和链上交互的实体)、proxies(contract在本地的代理, 方便调用)、注册的filter以及相应的event msg 的 decoder +2. raiden service : 转账操作的实体, 由俩部分构成 transport(目前是UDP):发送线下交易的实体、NodeState:这部分包含 storage(记录routing gragh、balance proof、locked transfer、token networking), handler例如 handler_direct_transfer(partner线下转账), handler_new_balance(partner在contract新增一笔押金), handler_block(blocknum增加了, locked_transfer 是否还安全否则需要关闭channel去体现了)等 +3. http service : 接受处理转账等http 请求 +4. alarm service: 不断的获取最新的blocknum后执行注册的callback list + + + +### 交易的最简流程 ++ sendTransaction(create netting channel) ++ 在alarmService处注册监听netting_channel 的事件的handler: poll_netting_channel_event, poll的具体步骤blockchain_service.eth_client.filter(from, to, netting_channel_address)) ++ sendTransaction(channel.deposit) ++ alarmService poll blocknum后调用注册的callback ++ raiden_service.handler_channel_new_deposit(event)其结果是给对方账户上加钱 ++ curl httpserver.direct_transfer ++ httpserver调用raiden_service.direct_transfer ++ transport.send ++ 接收方raiden_service.handler_direct_transfer + + +### A给B的最大可转账金额 +```python + +class Store: + nodeId_to_transfer_amount = {} + nodeId_to_balance = {} + + @classmethod + def set_transfer_amount(cls, addr, transfer_amount): + prefix = "transfer-amount:" + key = "{}-{}".format(prefix, addr) + party = nodeId_to_transfer_amount.get(key, None) + if party: + nodeId_to_transfer_amount[key] = balance + + @classmethod + def get_transfer_amount(cls, addr): + prefix = "transfer-amount:" + key = "{}-{}".format(prefix, addr) + return nodeId_to_transfer_amount.get(key, 0) + + @classmethod + def get_deposit(cls, addr): + prefix = "balance:" + key = "{}-{}".format(prefix, addr) + return nodeId_to_balance.get(key, ()) + + @classmethod + def set_deposit(cls, addr, value, blocknum): + v = (value, blocknum) + prefix = "balance:" + key = "{}-{}".format(prefix, addr) + nodeId_to_balance[key] = v + + @classmethod + def get_endport(cls, addr): + prefix = "endpoint:" + key = "{}{}".format(prefix, addr) + return endpoint.get(key, "") + + @classmethod + def set_endport(cls, addr, host_port): + prefix = "endpoint:" + key = "{}{}".format(prefix, addr) + return endpoint.set(key, host_addr) + +def get_balance(sender, receiver): + return sender的质押金额 - 转出去的金额 + 收到的金额 +def get_amount_locked(end_state): + return 所有解锁了的锁定金额 + 所有锁定的金额 +def get_distributable(sender, receiver): // sender的可转账资金 + return get_balance(sender, receiver) - get_amount_locked(sender) // 锁定的不能花, 解锁的已经花了 + +``` + +### direct transfer 的模拟过程 +```python + +q = Queue() +not_stop = True +transport = UdpServer(127.0.0.1, 33456) +class UdpServer: + def __init__(self): + self._server = DatagramServer(host, port, self._receive) + def _receive(self, data, host): + msg = decode(data) + if msg["type"] = "ping": + pass + elif msg["type"] = "received": + pass + elif msg["type"] = "direct_transfer": + handle_direct_transfer(msg) + def send(self, msg, receiver): + self._server.sentTo(msg, receiver) + +def sending(blocking=True) + def runner(): + while not_stop: + pair = q.peek(blocking): + if pair: + signed_msg = sign(pair[0]) + transport.send(signed_msg, pair[1]) + q.get() + else: + sleep() + run_at_new_thread(runner) + +def async_send_msg(msg): + q.put(msg) + +def transfer_direct(amount, receiver): + available_amount = get_distributable(nodeId, receiver) + if available_amount >= amount: + msg = create_balance_proof(value) + host_port = Store.get_endport(receiver, "") + if host_port: + async_send_msg((msg, host_port)) + return "ok" + return "found not not available" + +def handler_direct_transfer(sender, msg): + varify(msg) // 验证签名 + available_amount = get_distributable(sender, nodeId) + if available_amount > msg['amount']: + return "{}转账, 但是资金不足".format(sender) + last_transfer_amount = Store.get_transfer_amount(sender) + if last_transfer_amount>=msg["transfer_amount"]: + return "{}非法请求".format(sender) + Store.set_transfer_amount(sender, msg["amount"]) + return "收到{}转账{}".format(sender, msg["amount"]-last_transfer_amount) + +def create_balance_proof(value): + nonce = get_nonce() + transfered_amount = Store.get_transfer_amount(nodeId) + locked_root = get_loocked_root() + msg = encode(value, nonce, locked_root) + data = {"hash":hash(msg), "nonce":nonce, "amount":transfered_amount+amount, "locked_root":locked_root, "type":"direct_transfer"}} + Store.set_transfer_amount(nodeId, transfered_amount+amount) + return data + +sending() +transfer_direct(10, receiver) + +``` + + +### blockchain event的处理流程(处理同一个channel内的countpanty deposit事件) +```python + +nodeId = hash("self pub key") +event_callbacks= [] + +def decode(msg): + if msg.topic == "channel_new_balance": + return {"token":"resp from web3", balance:1, party:"resp from web3"} + return {} + +def poll_contract_new_balance(from, to, channel_addr): + events = eth.filter({from, to, addr}) + for event in events: + msg = decode(event) + if msg["topic"] = "new_deposit": + handle_new_deposit(msg["sender"], msg["value"], blocknum) + +def is_safe_transaction(blocknum): + return True if blocknum + 5 < current_blocknum else False + +def handler_new_deposit(sender, value, blocknum): + while is_safe_transaction(blocknum): + break + v = Store.get_deposit(sender) + if v[1] >= blocknum: + //log("本地存储的数据和blockchain 上的数据冲突, 本地记录 在{}块 deposit:{}, blockchain:{}块deposit:{}".format{v[0], v[1], blocknum, value}) + exit("-1") + Store.set_deposit(sender, value, blocknum) + return "ok" + +def reg_callbacks(cb): + event_callbacks.append(cb) + +def alarm(): + current_block_blocknum = snap.current_blocknum + while True: + num = poll_block_num() + if num > current_block_blocknum: + run_at_new_thread(event_callbacks) + sleep(0.5) +reg_callbacks(poll_contract_new_balance) +alarm() + +``` + diff --git a/scalability/raiden/imgs/smart_contract_obj.png b/scalability/raiden/imgs/smart_contract_obj.png new file mode 100644 index 0000000..4807004 Binary files /dev/null and b/scalability/raiden/imgs/smart_contract_obj.png differ diff --git a/scalability/split.py b/scalability/split.py new file mode 100644 index 0000000..ef74f52 --- /dev/null +++ b/scalability/split.py @@ -0,0 +1,102 @@ + # -*- coding=UTF-8 -*- +import hashlib +import math +import itertools +import collections +Block = collections.namedtuple("Block", ["root", "parts", "size"]) +Part = collections.namedtuple("Part", ["part1", "part2", "proofs"]) +Proof = collections.namedtuple("Proof", ["index", "proof"]) + +def split_in_fixed_size(data, fixed_size=10): + data += "".join([" " for i in range(fixed_size-1)]) + return [data[i*fixed_size:(i+1)*fixed_size] for i in range(int(len(data)/fixed_size))] + +def make_pairs(_nodes): + nodes = iter(_nodes) + for e in itertools.zip_longest(nodes, nodes): + yield e + +def hash_pair(pair): + md5 = hashlib.md5() + md5.update(pair[0].encode("utf-8")) + md5.update(pair[1].encode("utf-8")) + return md5.hexdigest() + +def build_merkel_tree(nodes): + start = 0 + while True: + uplevel = [] + for pair in make_pairs(nodes[start:]): + uplevel.append(hash_pair(pair)) + start = len(nodes) + nodes.extend(uplevel) + if len(uplevel) == 1: + return nodes + return [] + + +def patch_balanced_btree_leaves(leaves): + if math.pow(2, int(math.log(len(leaves), 2))) == len(leaves): + return leaves + btree_leaves_len = int(math.pow(2, int(1+math.log(len(leaves), 2)))) + return leaves + ["" for i in range(btree_leaves_len-len(leaves))] + +def build_proof_paths(tree_len): + proof_indexes = [0] + for i in range(tree_len-1): + for j in range(int(math.pow(2, i)), int(math.pow(2, i+1)), 2): + idx = j + father = int(j/2) + uncle = father - 1 + if father % 2== 1: + uncle = father+1 + if uncle < 0: + uncle = 0 + if j>1: + idx += 1 + proof_indexes.append([proof_indexes[uncle], idx]) + proof_indexes.append([proof_indexes[uncle], idx+1]) + proof_paths = proof_indexes[int(math.pow(2, tree_len-2))-1:int(math.pow(2, tree_len-1))-1] + proof_paths = proof_paths[::-1] + for i in range(0, len(proof_paths) ,2): + proof_paths[i], proof_paths[i+1] = proof_paths[i+1], proof_paths[i] + return proof_paths + +def flatten_list(path): + rt = [] + for e in path: + if isinstance(e, list): + rt.extend(flatten_list(e)) + else: + rt.append(e) + return rt + +def build_block_parts(data): + leaves = split_in_fixed_size(data) + raw_leaves_len = len(leaves) + leaves = patch_balanced_btree_leaves(leaves) + nodes = build_merkel_tree(leaves) + block = Block(nodes[len(nodes)-1], [], raw_leaves_len) + leaves_len = len(leaves) + paths = build_proof_paths(int(math.log(len(nodes)+1, 2))) + nodes_len = len(nodes) + leaf_pairs = list(make_pairs(leaves[:raw_leaves_len])) + nodes = nodes[::-1] + for i in range(int(raw_leaves_len/2)): + path = flatten_list(paths[i])[::-1] + proofs = [Proof(j, nodes[j]) for j in path] + part = Part(leaf_pairs[i][0], leaf_pairs[i][1], proofs) + block.parts.append(part) + return block + +def verif_part(part, root): + if root != part.proofs[len(part.proofs)-1].proof: + return False + mediate_hash = hash_pair([part.part1, part.part2]) + proof_len = len(part.proofs) + for idx in range(0, proof_len-1): + pair = ([mediate_hash, part.proofs[idx].proof]) + if part.proofs[idx].index%2==0: + pair = pair[::-1] + mediate_hash = hash_pair(pair) + return True if mediate_hash == root else False diff --git a/scalability/tendermint.md b/scalability/tendermint.md new file mode 100644 index 0000000..02abeeb --- /dev/null +++ b/scalability/tendermint.md @@ -0,0 +1,117 @@ +##### tendermint 使用 BFT-Like 共识算法出块, 本文将阐述其实现中一些技巧 +1. proposer 将要block 拆分为proposal 和 parts, 拆分算法可简单归结为如下 ++ _序列化Block, 获取hash值, proposal的主体内容有 block_hash、height(投票高度), round(当前高度下第几轮投票)_ ++ _将序列化的block拆分为[][]byte数组_ ++ _依据以上数组创建merkel tree_ ++ _依据叶子节点及其merkel proof创建part, merkel proof 可归结使用叶子节点及其叔父节点推导祖父节点的重复过程直至推导出root节点 +[算法实现python版](https://github.com/XChainLab/documentation/tree/master/scalability/split.py) + +拆分的作用解释 ++ _节点之间可以相互同步part, 完整性验证可由merkel proof 保证, 避免了消息下发中心化的问题_ ++ _第二、三轮的投票仅需proposal头部即可, 消息体尺寸极大的降低了_ + +2. 通过维护远程节点的镜像来同步Message, 实现过程是:在于远程节点连接后,本地节点开启三个同步Routine及一个订阅了本地事件消息的broker来维持节点间的消息同步 ++ _gossipDataRoutine负责block、tx、proposal同步_ ++ _gossipVoteRoutine负责vote转发, 作用有二: 1发现BFT节点, 2加快出块(原因有二:1消息是SignedMessage, 所以我们无需担心篡改的问题, 2我们只关心超过2/3正直的节点做出了选择即可发起下一轮投票或者saveBlock)_ ++ _gossipMaj23Routine同步投票镜像,为VoteRoutine同步投票提供依据_ ++ _消息broker订阅EventRoundStep、EventVote、 EventProposalHeartbeat, 其处理都是将相应的消息广播给远程节点, 用以维护本地节点在远程的镜像, 以维持消息同步_ + +3. 通过scheduleTimeout来简化投票超时的实现过程, 算法实现如下 +```raw +type timeoutInfo struct { + Duration time.Duration `json:"duration"` + Height int64 `json:"height"` + Round int `json:"round"` + Step cstypes.RoundStepType `json:"step"` +} +tickChan = make(chan timeoutInfo, BufSize) +tockChan = make(chan timeoutInfo, BufSize) +timer = time.Timer +func scheduleTimeout(t *timeoutInfo){ + tickChan <- t +} +func (self timeoutInfo)newer(other timeoutInfo)bool{ + if self.Height < other.Height { + return false + } else if newti.Height == ti.Height { + if self.Round < other.Round { + return false + } else if self.Round == other.Round { + if self.Step > 0 && self.Step <= other.Step { + return false + } + } + } + return true +} +func timeoutRoutine() { + var ti timeoutInfo + for { + select { + case newti := <-tickChan: + if !newti.newer(ti){ + continue + } + timer.Stop() + ti = newti + timer.Reset(ti.Duration) + case <-timer.C: + go func(toi timeoutInfo) { t.tockChan <- toi }(ti) + case <- quit(): + return + } +} +func someRoutinue(){ + for{ + select{ + case t := <- tockChan: + go handleTimeout(t) + ...... + } + } +} + +func handleTimeout(t timeoutInfo){ + .... + .... +} +``` + +4. 通过状态机转换来简化投票过程, 主要三个状态是proposal、prevote、precommit、以及主要状态各自对应的wait状态、另外在加开始的俩个状态NewHeight、NewRound以及结束的commit状态共计九个状态, 状态转换图如下 +```raw + + +-------------------------------------+ + v |(Wait til `CommmitTime+timeoutCommit`) + +-----------+ +-----+-----+ + +----------> | Propose +--------------+ | NewHeight | + | +-----------+ | +-----------+ + | | ^ + |(Else, after timeoutPrecommit) v | + +-----+-----+ +-----------+ | + | Precommit | <------------------------+ Prevote | | + +-----+-----+ +-----------+ | + |(When +2/3 Precommits for block found) | + v | + +--------------------------------------------------------------------+ + | Commit | + | | + | * Set CommitTime = now; | + | * Wait for block, then stage/save/commit block; | + +--------------------------------------------------------------------+ + + +``` + +5. 通过wal机制保证断点恢复, 状态转换前先write转换参数, 恢复时只需逐层load转换参数即可。 + +6. 惩罚BFT节点的算法 +```raw + 1. 开始Block高度为H的投票 + 2. 本地节点收到节点A的投票,将投票加入本地VoteSet集合 + 3. 收到节点B发来节点A的投票, 该投票与本地VoteSet中的投票冲突 + 4. 将冲突证据加入EvidencePool并广播证据 + 5. 高度为H的投票结束,开始执行BLock中Transaction并且依据Block的Evidence惩罚BFT节点, 开始H+1 的投票 + 6. proposer创建高度为H+1 Block, 将收到的冲突证据加入到Block中 + 7. 开始 2~5 +``` + diff --git "a/scalability/\350\267\250\351\223\276\347\232\204\345\272\224\347\224\250\345\260\217\346\200\273\347\273\223.docx" "b/scalability/\350\267\250\351\223\276\347\232\204\345\272\224\347\224\250\345\260\217\346\200\273\347\273\223.docx" new file mode 100644 index 0000000..2b1035c Binary files /dev/null and "b/scalability/\350\267\250\351\223\276\347\232\204\345\272\224\347\224\250\345\260\217\346\200\273\347\273\223.docx" differ diff --git "a/testDoc/\351\223\276\350\260\203\347\240\224\347\232\204\350\241\250.xlsx" "b/testDoc/\351\223\276\350\260\203\347\240\224\347\232\204\350\241\250.xlsx" new file mode 100644 index 0000000..ba6110d Binary files /dev/null and "b/testDoc/\351\223\276\350\260\203\347\240\224\347\232\204\350\241\250.xlsx" differ diff --git a/translate/IPFS/imgs/asymmetric-encryption.png b/translate/IPFS/imgs/asymmetric-encryption.png new file mode 100644 index 0000000..16f83aa Binary files /dev/null and b/translate/IPFS/imgs/asymmetric-encryption.png differ diff --git a/translate/IPFS/imgs/blockchain-used-ipfs.png b/translate/IPFS/imgs/blockchain-used-ipfs.png new file mode 100644 index 0000000..4500d69 Binary files /dev/null and b/translate/IPFS/imgs/blockchain-used-ipfs.png differ diff --git a/translate/IPFS/imgs/blockchain.png b/translate/IPFS/imgs/blockchain.png new file mode 100644 index 0000000..82b7925 Binary files /dev/null and b/translate/IPFS/imgs/blockchain.png differ diff --git a/translate/IPFS/imgs/check-key-importy.png b/translate/IPFS/imgs/check-key-importy.png new file mode 100644 index 0000000..27b52a6 Binary files /dev/null and b/translate/IPFS/imgs/check-key-importy.png differ diff --git a/translate/IPFS/imgs/decrypto-myriad.png b/translate/IPFS/imgs/decrypto-myriad.png new file mode 100644 index 0000000..3d1024a Binary files /dev/null and b/translate/IPFS/imgs/decrypto-myriad.png differ diff --git a/translate/IPFS/imgs/encrypt-myriad.png b/translate/IPFS/imgs/encrypt-myriad.png new file mode 100644 index 0000000..e0b13c8 Binary files /dev/null and b/translate/IPFS/imgs/encrypt-myriad.png differ diff --git a/translate/IPFS/imgs/example-on-linux.png b/translate/IPFS/imgs/example-on-linux.png new file mode 100644 index 0000000..769f853 Binary files /dev/null and b/translate/IPFS/imgs/example-on-linux.png differ diff --git a/translate/IPFS/imgs/get-pdf.png b/translate/IPFS/imgs/get-pdf.png new file mode 100644 index 0000000..db5615c Binary files /dev/null and b/translate/IPFS/imgs/get-pdf.png differ diff --git a/translate/IPFS/imgs/gpg-finished.png b/translate/IPFS/imgs/gpg-finished.png new file mode 100644 index 0000000..585d76a Binary files /dev/null and b/translate/IPFS/imgs/gpg-finished.png differ diff --git a/translate/IPFS/imgs/gpg-gen-key.png b/translate/IPFS/imgs/gpg-gen-key.png new file mode 100644 index 0000000..866d043 Binary files /dev/null and b/translate/IPFS/imgs/gpg-gen-key.png differ diff --git a/translate/IPFS/imgs/ipfs-add-myriad.png b/translate/IPFS/imgs/ipfs-add-myriad.png new file mode 100644 index 0000000..3ea29fb Binary files /dev/null and b/translate/IPFS/imgs/ipfs-add-myriad.png differ diff --git a/translate/IPFS/imgs/ipfs-daemon.png b/translate/IPFS/imgs/ipfs-daemon.png new file mode 100644 index 0000000..086624e Binary files /dev/null and b/translate/IPFS/imgs/ipfs-daemon.png differ diff --git a/translate/IPFS/imgs/ipfs-get-file.png b/translate/IPFS/imgs/ipfs-get-file.png new file mode 100644 index 0000000..bc3a910 Binary files /dev/null and b/translate/IPFS/imgs/ipfs-get-file.png differ diff --git a/translate/IPFS/imgs/ipfs-init.png b/translate/IPFS/imgs/ipfs-init.png new file mode 100644 index 0000000..fafde61 Binary files /dev/null and b/translate/IPFS/imgs/ipfs-init.png differ diff --git a/translate/IPFS/imgs/ipfs-pin-ls.png b/translate/IPFS/imgs/ipfs-pin-ls.png new file mode 100644 index 0000000..592c68d Binary files /dev/null and b/translate/IPFS/imgs/ipfs-pin-ls.png differ diff --git a/translate/IPFS/imgs/ipfs-workflow.png b/translate/IPFS/imgs/ipfs-workflow.png new file mode 100644 index 0000000..28d5c28 Binary files /dev/null and b/translate/IPFS/imgs/ipfs-workflow.png differ diff --git a/translate/IPFS/imgs/learn-to-share.png b/translate/IPFS/imgs/learn-to-share.png new file mode 100644 index 0000000..0f644c0 Binary files /dev/null and b/translate/IPFS/imgs/learn-to-share.png differ diff --git a/translate/IPFS/imgs/myriad.png b/translate/IPFS/imgs/myriad.png new file mode 100644 index 0000000..b0954e2 Binary files /dev/null and b/translate/IPFS/imgs/myriad.png differ diff --git a/translate/IPFS/imgs/type-random-bytes.png b/translate/IPFS/imgs/type-random-bytes.png new file mode 100644 index 0000000..8d2b9e8 Binary files /dev/null and b/translate/IPFS/imgs/type-random-bytes.png differ diff --git a/translate/IPFS/learn-to-securely-share-files-on-the-blockchain-with-IPFS.md b/translate/IPFS/learn-to-securely-share-files-on-the-blockchain-with-IPFS.md new file mode 100644 index 0000000..545e79b --- /dev/null +++ b/translate/IPFS/learn-to-securely-share-files-on-the-blockchain-with-IPFS.md @@ -0,0 +1,293 @@ +## 学习使用IPFS在区块链上安全分享文件 ## + +![learn to share](imgs/learn-to-share.png) + +在阅读本篇文章之前,建议先阅读我们的另外一篇文章,”用少于200行Go代码来编写一个你的区块链“。 + +近一段时间,大家对区块链的兴趣已经达到了狂热的阶段。 +尽管目前有很多来自例如加密货币和ICO等区块链应用的讨论, +但是,技术本身还是让人感觉到兴奋的。 +区块链提供了一个民主化的信任和验证协议,这个协议已经打破了传统的银行体系, +并且还给其他行业带来了巨大的变革,如医疗保健,金融服务,社交应用软件等。 + +然而,从技术角度看,区块链并非没有瑕疵。 +当前的工作量证明共识算法已经交易速度放缓到几近瘫痪的水平了。 +等待比特币的交易成功,让它基本不能出现在平台系统上, +而加密猫同样使得以太坊的网络近乎停滞。 + +这就使得在区块链上存储数据变得没有机会成为可能。 +如果区块链仅仅支持少量的关于交易双方的余额转移信息, +我们究竟如何将大文件或者是图片存放到区块链中呢? +我们是否必须将区块链的作用范围限制到小小的文本字符串的范围内? + +**了解IPFS** + +目前最具有前途的解决方案是由Protocol Labs提出的 [IPFS](http://ipfs.io/), 又名 Interplanetary File System. +这是一种点对点的协议,协议定义了每个节点存贮哈希文件的集合。 +想要检索这些文件的客户可以访问一个抽象的很好的结构来实现, +只需要调用想要得到的文件的哈希值即可。IPFS会仔细查找节点并将文件返回给客户端。 + +你可以认为它与BitTorrent相似。它是一种分布式存储和引用文件的方式, +但是给你提供了更多控制方式,还可以通过哈希来引用文件, +从而实现更丰富的程序化交互模式。 + +通过下面这个简单的图,你可以了解到IPFS的工作流。 + +![ipfs-workflow](imgs/ipfs-workflow.png) + +1. John想要将一份PDF文件上传到IPFS中 +1. 他把他的PDF放到他的工作目录中 +1. 他告诉IPFS,他想要添加这份文件到IPFS里,它会生成文件的哈希值 +(因为哈希值总是以Qm开头,所以你可以告诉IPFS) +1. 他的文件现在就可以在IPFS网络中得到了 + +现在假设John想要通过IPFS与他的同事Mary分享这个文件。 +他只需要告诉Mary从上面第三步中得到的哈希值。 +然后,Mary在反向操作1-4的步骤。 +Mary所需要做的就是从IPFS调用哈希,并获取PDF文件的副本。好帅气。 + +![Mary get PFD from IPFS](imgs/get-pdf.png) + +**安全漏洞** + +然而,这有一个显而易见的安全漏洞。 +任何人得到了PDF文件的哈希值,那就可以随意的在IPFS中获取文件副本。 +因此,敏感文件不合适存放在IPFS的生态中。 +在我们不做任何处理的情况下, +分享类似于健康记录或者图片之类的敏感文件是不合适的。 + +**应用非对称加密** + +幸运的是,我们有一次性的工具,可以很好的与IPFS配套使用, +在文件上传到IPFS之前就得到保护。 +非对称加密允许我们通过一个只有收件人才有的公钥来加密这份文件, +所以也就只有收件人才能从IPFS取回文件并解密。 +如果一个恶意的部分从IPFS中获取到文件,也不能做什么,以为他没办法解密。 +本教程中,我们将使用[GPG](https://www.gnupg.org/)非对称加密算法。 +让我们修改下带有加密解密的流程图,如下: + +![asymmetric encryption](imgs/asymmetric-encryption.png) + +1. John上传一份文件到IPFS,但是只希望Mary可以查看到 +1. 他使用了Mary提供的公钥将文件在本地目录中进行加密 +1. 他告诉IPFS,他想要添加这份*加密*文件到IPFS,同时生成了加密文件的哈希 +1. 他加密过的文件已经存放到IPFS网络中了 +1. Mary可以获取到文件,并因为她拥有公钥生成的私钥可以用来解密这份文件 +1. 其他恶意部分人员因为没有Mary的私钥不能够解密这份文件 + +## 区块链 ## + +那么区块链怎么适合呢?在我们继续之前,我们建议你选阅读下我们之前的文章: +”使用少于200行的GO代码编写你自己的区块链“。 + +如下的图非常重要: + +![blockchain](imgs/blockchain.png) + +请注意BPM部分。这种简单的文本记录是目前所有区块链都可以处理的。 +也是为什么加密货币很适合区块链的原因。你只需要记录交易的发送方,接收方 +和开始交易的比特币(包括以太坊等等)数量。 +因为所有这些哈希值都需要计算和验证以保持链的完整性,所以区块链很*可怕*, +在将文件或者大量数据存储到块中的时候将会变得非常*可怕*。 + +这也是为什么IPFS和区块链相结合的时候就会变得强大的原因。 +替换掉前面的BPM,我们只存储IPFS文件的哈希值!真的是太cool了。 +我们只在区块链中存储简单的数据, +同时我们享受了IPFS提供的文件存储和分散式点对点的特性。真是两全其美。 +由于我们同时使用非对称加密的方式(GPG)增加了安全性, +因此,我们在区块链中”存储“,加密,共享大量数据和文件的方式将会变得非常优雅。 + +![block used ipfs](imgs/blockchain-used-ipfs.png) + +一个现实世界的应用就是在每一个区块中存储我们的健康记录和实验结果。 +当我们获得新的实验结果时,我们只需要创建一个新块, +它的内容指向IPFS中的加密图像或者实验结果的PDF文件。 + +## 了解的已经足够多。我们开始展示如何做的吧! ## + +记下来的教程中我将会执行如下步骤: + +* 设置GPG +* 设置IPFS +* 通过别人的公钥来加密文件 +* 上传文件到IPFS +* 在另外一台机器(或者虚拟机)上下载文件,并且使用确定的私钥进行解密并查看内容 + +### 你需要如下东西 ### + +* 另外一台计算机或者一个虚拟机。 +*另外一台计算机模拟另外一个你需要通过安全方式进行分享文件的人* +* 一个测试文件。我们建议下载这份[文件](https://s3.amazonaws.com/myriad-web/Managed+Care/IntegratedBRAC_Negative-SampleReport.pdf), +这是一份PDF版本的实验报告。 +这是一个很好又切合实际的例子,因为我们是一家医疗保健公司, +我们需要保护敏感的个人信息。**把这份文件放到你的本地文件夹中** + +好了!我们开始吧! + +## 创建 ## +### GPG ### + +首先让我们在两台电脑上都下载GPG. + +挑选与你相同的操作系统,按照[安装文章](http://blog.ghostinthemachines.com/2015/03/01/how-to-use-gpg-command-line/)中的说明进行操作。 +如果你是Mac,假设你安装了Homebrew, +最简单的方法就是打开你终端执行`brew install gnupg`命令。 + +在每一台电脑安装完GPG之后生成一个key。使用如下步骤: + +`gpg --gen-key` 根据提示选择默认的方式。确保你的用户名和电子邮箱选择的密码都已经安全的记录或者存储下来了。 + +![gpg --gen-key](imgs/gpg-gen-key.png) + +(译者注:我自己试验了下,如下图) +![gpg --gen-key on linux](imgs/example-on-linux.png) + +到这个阶段,gpg会要求你做一些随机的事情来生成加密使用的熵。 +我只是输入一堆随机字符,直到生成完成。 + +![type random bytes](imgs/type-random-bytes.png) + +![gpg finished](imgs/gpg-finished.png) + +当第二台电脑上的key也创建完成后, +我们需要将生成key添加到第一台电脑的Keyring(译者注:秘钥环)中, +以便我们可以加密之后第二台电脑可以解密的文件。 + +使用你在创建 key 的时候选择的电子邮件地址 +将第二台电脑上 public key 导出到一个二进制形式文件中。 + +`gpg --export --armor -email > pubkey.asc` + +将文件`pubkey.asc`移动到你的第一台电脑里。并确定是安全的转移。 +例如使用U盘会比使用邮件更好。 + +当文件`pubkey.asc`迁移到你的本地文件目录后,使用如下方法导入到秘钥环中: + +`gpg --import pubkey.asc` + +使用`gpg --list-kyes`的命令来检查导入是否正确。 +我的第二台电脑就叫做 Cory Heath,看起来是正确的: + +![gpg --list-keys](imgs/check-key-importy.png) + +好极了!我们已经完成GPG的配置,现在我们去配置IPFS。 + +### IPFS ### + +根据你的系统选择合适的[文档](https://ipfs.io/docs/install/)在你准备使用的两台电脑上进行安装IPFS。 +当安装完成,使用命令`ipfs init`在两台电脑上进行初始化, +并使用命令`ipfs daemon`让两台电脑上的IPFS都在后台运行起来。 + +![ipfs init](imgs/ipfs-init.png) + +![ipfs daemon](imgs/ipfs-daemon.png) + +非常好!现在我们的准备工作完成了。我们要开始加密文件和上传文件了。 + +### 加密 ### + +还记得刚刚我们下载的实验报告么?请将这个文件移动到你的第一台电脑的本地工作区。 + +让我们开始加密文件(因为文件来自于Myriad Genetics,我先将文件的名字修改成`myriad.pdf`),加密使用的是第二台电脑的公钥(在这个例子中,叫做Cory Heath) + +`gpg --encrypt --recipient "Cory Heath" myriad.pdf` + +![encrypt myriad](imgs/encrypt-myriad.png) + +如果使用`ls`命令来查看文件夹,你会发现一个加密后的文件,叫做`myriad.pdf.gpg` + +现在只有你的第二台电脑可以解密并查看这个文件。试一试! +通过Email发送给其他朋友,让他们试着解开,你会发现他们并不能打开文件! +即使将文件修改回原来为`myriad.pdf`的名字。 + +![encrypt myriad](imgs/myriad.png) + +我们已经得到了加密的文件,接下来让我们把它传到IPFS上。 + +### 上传到IPFS ### + +在第一台电脑中使用如下命令将文件上传到IPFS中 + +`ipfs add myriad.pdf.gpg` + +我们应该得到如下的显示内容: + +![ipfs add myriad](imgs/ipfs-add-myriad.png) + +Qm...开头的字符串就是文件的哈希值。 +你可以将这个哈希值分享给你朋友们,或者任何一个希望通过IPFS查看文件的人。 + +让我们再一次确认,保证文件已经在IPFS上了,可以使用`ipfs pin ls`命令: + +![pifs pin ls](imgs/ipfs-pin-ls.png) + +你可以看到,我们的文件哈希值确实存在,现在可以在IPFS上使用了! + +### 从IPFS下载 ### + +我们切换到第二台电脑上。记住,我们是在模拟另外一个人。 +为了让其更加真实,可以与朋友分享这篇教程后交换电脑使用。 + +在我们这个例子中,我们使用的是带有vagrant的 Ubuntu VM, +而不是第二台电脑。 +这*不*是要求。 + +在第二台电脑中,使用相同的哈希值下载你从第一台电脑中上传的加密文件: + +`ipfs get QmYqSCWuzG8Cyo4MFQzqKcC14ct4ybAWyrAc9qzdJaFYTL` + +如下图就是已经下载成功了: + +![download encrypted file](imgs/ipfs-get-file.png) + +### 解密 ### + +由于我们现在在第二台电脑上,我们使用的是第二台电脑的公钥进行的加密, +理论上,我们可以使在这台电脑上进行解密,并查看文件内容。 + +我们试一试。 + +解密下载的文件,让我们修改他的名字为`myriad.pdf` + +`gpg --decrypt QmYqSCWuzG8Cyo4MFQzqKcC14ct4ybAWyrAc9qzdJaFYTL > myriad.pdf` + +**见证奇迹的时刻:** + +如果一切运行正常,当我们在另外一个台电脑上打开文件时,我们会看到文件内容。 + +`open myriad.pdf` + +![my riad](imgs/decrypto-myriad.png) + +TADA!我们成功下载,解密并打开了我们的文件,该文件在IPFS上是完全加密存储的, +并且对于访问内容的任何人来说都是被保护起来的。 + +### 总结和下一步 ### + +先鼓励下自己。我们刚刚完成的功能非常强大,解决了当今区块链中一些关键问题。 + +让我们快速回顾下我们刚做的事情: + +* 意识到区块链中存储大量数据和文件会及其不美观 +* 设置IPFS,并运行,连接成网络 +* 使用GPG保护敏感文件,并将文件存储到IPFS上 +* 了解IPFS中的哈希值,以及我们如何在区块链中使用分布式文件系统,使得二者更有力的融合 + +之前的内容能学到多少全完取决你自己。其中有很多可以单独成为分支的部分。 +可以考虑将这些例子部署到你们自己的IPFS活跃节点上,用以存放重要数据。 +IPFS的缺点是,当你的文件不是足够的流行,当你的节点停止服务, +那么文件也将从IPFS的网络上消失。你可以使用云服务器来当做一个IPFS节点, +先让自己存储,直到更多节点对你的文件感兴趣并存储了它们。 + +再次查看我们之前的”编写你自己的区块链“教程,一、二、三、四 4个部分。 +一点都完成,你可以试着将IPFS和你自己大型加密文件区块链相结合。 + +同时,告诉我们接下来你想看到的内容!我们非常喜欢这些区块链相关的技术教程。 +与我们联系的最好方式是我们的 [telegram](https://t.me/joinchat/FX6A7UThIZ1WOUNirDS_Ew) +和 [twitter](https://twitter.com/myCoralHealth)! +我们很乐意听取到你的意见! + +要了解更多的关于 Coral Health 的相关内容以及我们是如何使用区块链推进个性化医学眼角的, +请访问我们的[网站](https://mycoralhealth.com/)。 + +原文地址:https://medium.com/@mycoralhealth/learn-to-securely-share-files-on-the-blockchain-with-ipfs-219ee47df54c \ No newline at end of file diff --git a/translate/consensus/A_Hitchhiker's_Guide_to_Consensus_Algorithms.md b/translate/consensus/A_Hitchhiker's_Guide_to_Consensus_Algorithms.md new file mode 100644 index 0000000..26d3520 --- /dev/null +++ b/translate/consensus/A_Hitchhiker's_Guide_to_Consensus_Algorithms.md @@ -0,0 +1,131 @@ +### 共识算法漫游指南(A Hitchhiker's Guide to Consensus Algorithms) +加密货币共识类型快速分类 + +莫慌。在每一种伟大的电子货币的背后,都有一个伟大的共识算法来支撑。 +没有一种共识算法是完美的,但是每一种都有他们自己的优点。 +在加密的世界里,共识的存在就是为了解决 *双花* 的问题。 +接下来,简单介绍一些迄今为止最为流行的共识算法,从Blockchains到DAG之间的区别。 + +--- + +#### 工作量证明(PoW)- 元老级共识 + +![Alt text](imgs/pow.png) + +**广泛应于与:** Bitcoin, Ethereum, Litecoin, Dogecoin(大多数其他加密货币) + +**优点:** 我们知道他的工作量(译者:双关语,我们知道它运转正常) + +**缺点:** 低吞吐量;正在杀死地球(译者:浪费资源) + +工作量证明是首个用于区块链的共识算法。由中本聪(Satoshi Nakamoto)设计,并应用于比特币区块链,我们能使用PoW还是要感谢在世界各地所看到的大规模采矿作业和能源的消耗。我们知道它运转正常(我们可以说它比其他大多数的共识运行的更好),但是,在这个游戏的这个阶段(译者:区块链技术萌芽阶段),它开始被认定是一个传统技术。甚至于以太坊也是从PoW迁移到更高效、更节能的PoS中。有了这么多新的方案,很难想象为什么一个新的区块链还会使用PoW。 +在PoW中,*矿工* 解决困难的并且无意义的问题来创造区块。PoW运行在一个“赢得最长链”的系统上。因此,假设大多数矿工工作在同一条链上,那么增长速度最快的将会是最长的也是最值得信赖的链。因此,只要比特币中长的链超过50%的矿工都是诚实的,它就是安全的。 + +--- + +#### 权益证明(POS)- 链中的新生儿 + +![Alt text](imgs/pos.png) + +**广泛应用于:** Decred,Ethereum(即将),Peercoin + +**优点:** 攻击成本更昂贵;更非中心化;高效节能 + +**缺点:** 无成本投票 + +在PoS中,块的产生不是由矿工工作产生的,而是由*矿工们持有的*他们的权益(token)来“下注”哪一块是有效的。在分叉的情况下,矿工们花费他们的权益(token)来投票支持哪一个分叉。假设大部分人投票到正确的分支上,投票到错误分支的验证者将会在正确的分支上“失去他们的权益”。 +关于权益证明常见的反对观点是“没有成本”的问题。值得注意的是,不同于PoW,验证者几乎不消耗计算能力去支持一条分叉,验证者可以投票到分叉的任何一条链上。PoS中的分叉也会比在PoW中更为常见,这也是人们担心会损害货币可信度的地方。 + +--- + +#### 授权证明(DPoS)- 选择你的验证节点 + +![Alt text](imgs/dpos.png) + +**广泛应用于:** Steemit, EOS, BitShares + +**优点:** 交易费用低;可扩展;高效节能 + +**缺点:** 局部中心化 + +DPoS是Daniel Larimer的智慧结晶,世界上与PoS完全不同。在DPoS中,token持有者不会对块本身的有效性进行投票,而是投票选举代表来代表他们自己去验证。在DPoS系统中,通常会有21-100个选出的代表。代表会定期洗牌,并且会将顺序记载到他们的块中。因为拥有较少的代表,他们可以高效的管理自己,并能每一位代表创建指定时间窗口来推送他们的块。如果代表不断的失误或者发布无效交易,股东会将他们投票出去,并找个更好代表代替他。 + +--- + +#### 权威证明(PoA)- 相信知道的所有 + +![Alt text](imgs/poa.png) + +**广泛应用于:** POA.Network, Ethereum Kovn testnet + +**优点:** 高吞吐率;扩展性 + +**缺点:** 中心化系统 + +权威证明是一种交易由类似于系统”管理员“的批准用户来验证块的共识算法。这些账户从其他节点提供的真相来证实的。PoA具有很高的吞吐量,并优化了私有网络。但由于过于集中,你不太可能在公链中看到这种共识。 + +--- + + +#### 权重证明(PoWeight)- 更大的就是更好的 + +![Alt text](imgs/poweight.png) + +**广泛应用于:** Algorand, Filecoin, Chia + +**优点:** 可定制;可扩展 + +**缺点:** 激励模式是个挑战 + +权重证明是基于Algorand一致性模型的共识算法分类中的常用分类。总体思路是,在PoS中,网络中你拥有的价值(token)代表了你”发现“下一个块的概率,在PoWeight系统中,使用了其他相对的加权值。具体的例子:Filecoin中的Proof-of-Spacetime会根绝你存储的IPFS数据进行加权。其他的系统可以包含的例如Proof-of-Reputation(声誉证明)之类的加权。 + +--- + +#### 拜占庭算法(BFT) - 围攻区块链 + +![Alt text](imgs/bft.png) + +**广泛应用于:** Hyperleder, Stellar, Dispatch, 和 Ripple + +**优点:** 高吞吐率;低消耗;可扩展 + +**缺点:** 半数信任 + +这是个经典的分布式计算问题,通常用拜占庭将军来解释。问题描述了几个拜占庭将军以及他们的军队,和被围攻的城的内容。他们必须一致决定是否进攻。如果将军们分别进攻,他们对城市的围困将会以失败告终。将军们按照距离分隔开,不得不用消息来进行沟通。一些加密货币协议使用了不同版本的BFT来达成共识,每种都有自己的有点和缺点。 + +__实用的拜占庭容错算法(PBFT):__ 其中一种优先解决方案叫做实用的拜占庭容错算法。Hyperledger Fabric目前正在实用,只设置很少(<20,之后可能会多一点)预先选定的将军执行PBFT,并表现出难以置信的高效。优点:高交易吞吐量;缺点:中心化/被许可。 + +__联邦拜占庭协议(FBA):__ FBA是另外一种拜占庭将军的解决办法,已经应用在Stellar和Ripple两种货币中。总体思想(heh,译者注:作者原文heh,可能想表达双关,将军的想法),是每一个将军负责自己的链,按照接收到真相排列消息。在Ripple中,将军们(产出 验证节点)是通过Ripple基金会预选出来的。在Stellar中,任何人都可以成为验证者,所以你可以选择你要相信的验证者。 +由于难以置信的吞吐量,低交易成本和网络扩展性,我相信,FBA类的一致性算法是我们目前发现最好的分布式共识算法。 + +--- + +#### 有向无环图(DAGs)- 又名区块链杀手 + +![Alt text](imgs/dags.png) + +**广泛应用于:** Ioa, Hashgraph, Raiblocks/Nano + +**优点:** 网络扩展性;低消耗 + +**缺点:** 依赖实施 + +DAGs现在比 Vitalik's Tinder profile 更火爆。DAGs是不使用区块链结构,并且绝大多数是异步处理事务的一种共识形式。即使DAGs能够非常专业在每秒处理无限次交易,但它还是其他共识一样具有优势和劣势。 + +__Tangle:__ Tangle是由Iota使用的DAG共识实现。为了发送一条Iota交易,你需要验证你已经收到的以前的两条交易信息。合二为一,并传递下去的这种共识方式增强了交易的有效性,更多的交易被添加到Tangle中。因为共识是由交易确定的,理论上,如果有人能生成1/3的交易,那么他们就可以说服网络上其他人,他们的无效交易是有效的。直到有足够多的交易数量,使得再创造1/3的交易数据变得不可能。Iota是网络交易中“双重检查”的一种形式,并运行在一个叫做“The Coordinator”的中心节点上。Iota表示 The Coordinator像是系统的辅助轮一样,当Tangle足够大的时候就会被移除。 + +__Hashgraph:__ Hashgraph是由Leemon Baird开发的一种gossip-protocol共识算法。节点随机地与其他节点共享已知的交易记录,因此最终交易记录都会同步到其他的所有节点中。Hashgraph速度非常的快(每秒250,000+笔交易),但无法抵挡Sybil攻击。所以,Hashgraph是成为私有网络的很好的选择,你一定不会看到它像Ethereum或Dispatch这样的共有网络中实现这种算法。 + +__Block-lattice:__ Nano(以前被称为Raiblocks)运行在 +一条叫做Block-lattice的链上。Block-lattice是一种结构,这种结构是让每一个用户(地址)都拥有他们自己的链,并且只有他们可以写入内容,每个人都拥有所有链的副本。所有的交易都被分解,并同时分配到一个发送者的发送块和一个接受组织的接接收块中。Block-lattice看起来简单到不能工作,但是它已经在外运行了一段时间了。独特的结构确实使得Block-lattice需要应对一些特殊的攻击,例如Penny-spend攻击,这种攻击是攻击者通过发送大量的、必须要追踪的、仅有少数价值的空钱包达到攻击手段。 + +__SPECTRE:__ *序列化的工作量证明事件:通过递归选举进行交易确认*,更广为人知的是SPECTRE,它是一个已经提出实用PoW和DAGs组合达成共识的比特币扩展方案。在SPECTRE中,区块被挖出后指向多个父区块,而不仅仅是一个,所以网络需要有能力每秒处理多个区块。即将被挖出来的块指向多个父区块,这些父区块能够证明其有效性。与PoW的"赢得最长链"相比,SPECTRE使用的更像是“赢得最多子区块”。SPECTRE还没有对外进行battle-tested,可能会出现新的攻击方式,但是,它看起来确实是潜在的能够巧妙的修复比特币的方式。 + +--- + +*我有错过你喜欢的算法么?喜欢这篇文章么?反馈到作者会让作者感动!由衷希望你们能获得我写这篇文章时我所得到的收获。十分感谢我的团队在Dispatch Labs(某个实验室)帮助我修改并筛选最好的公式算法!郑重感谢Hal Finney 🙏 并感谢所有的区块链建设者,让我们能走到这么远❤️* + +*如果发现这篇文章对你有帮助,请👏或分享出去。Fllow me on Twitter for more insights twitter.com/ZaneWithSpoon* + +原文:https://hackernoon.com/a-hitchhikers-guide-to-consensus-algorithms-d81aae3eb0e3 + diff --git a/translate/consensus/imgs/bft.png b/translate/consensus/imgs/bft.png new file mode 100644 index 0000000..a54a94d Binary files /dev/null and b/translate/consensus/imgs/bft.png differ diff --git a/translate/consensus/imgs/dags.png b/translate/consensus/imgs/dags.png new file mode 100644 index 0000000..3e6132c Binary files /dev/null and b/translate/consensus/imgs/dags.png differ diff --git a/translate/consensus/imgs/dpos.png b/translate/consensus/imgs/dpos.png new file mode 100644 index 0000000..31cebe9 Binary files /dev/null and b/translate/consensus/imgs/dpos.png differ diff --git a/translate/consensus/imgs/poa.png b/translate/consensus/imgs/poa.png new file mode 100644 index 0000000..74a17a4 Binary files /dev/null and b/translate/consensus/imgs/poa.png differ diff --git a/translate/consensus/imgs/pos.png b/translate/consensus/imgs/pos.png new file mode 100644 index 0000000..e4f826a Binary files /dev/null and b/translate/consensus/imgs/pos.png differ diff --git a/translate/consensus/imgs/pow.png b/translate/consensus/imgs/pow.png new file mode 100644 index 0000000..b9ab4f6 Binary files /dev/null and b/translate/consensus/imgs/pow.png differ diff --git a/translate/consensus/imgs/poweight.png b/translate/consensus/imgs/poweight.png new file mode 100644 index 0000000..35a336d Binary files /dev/null and b/translate/consensus/imgs/poweight.png differ diff --git "a/\347\233\256\345\275\225\347\273\223\346\236\204\345\222\214\347\274\226\347\240\201\350\247\204\350\214\203" "b/\347\233\256\345\275\225\347\273\223\346\236\204\345\222\214\347\274\226\347\240\201\350\247\204\350\214\203" new file mode 100644 index 0000000..eb561ed --- /dev/null +++ "b/\347\233\256\345\275\225\347\273\223\346\236\204\345\222\214\347\274\226\347\240\201\350\247\204\350\214\203" @@ -0,0 +1,371 @@ + + +目录结构规范 + +NAME +├── README.md 介绍软件及文档入口 +├── bin 编译好的二进制文件,执行./build.sh自动生成,该目录也用于程序打包 +├── build.sh 自动编译的脚本 +├── doc 项目的文档 +├── pack 打包后的程序放在此处 +├── pack.sh 自动打包的脚本,生成类似xxxx.20170713_14:45:35.tar.gz的文件,放在pack文件下 +└── src 项目的源代码 + ├── api 接入管理 + ├── node 节点管理 + ├── consensus 共识机制 + ├── core 核心功能 + ├── account 账户 + ├── asset 资产 + ├── blockchain 区块链 + ├── transaction 交易 + ├── validator 验证 + ├── witness 证明 + └── state 状态 + ├── policy 政策合规 + ├── crypto 签名加密 + ├── config 配置管理 + ├── vm 虚拟机 + ├── smartcontract 智能合约 + ├── storage 存储 + ├── cache 内存缓存 + ├── database 数据持久化 + ├── sql + ├── nosql + └── file + ├── json json格式 + └── wrapper 封装 + ├── network 对等网络 + ├── wallet 钱包接口 + └── vendor 依赖库 + ├── github.com/xxx 第三方库 + └── xxx.com/obc 公司内部的公共库 + + + +编码规范 (Golang) + + +文件名命名规范 + +用小写,尽量见名思义,看见文件名就可以知道这个文件下的大概内容,对于源代码里的文件,文件名要很好的代表了一个模块实现的功能。 + + +命名规范 + +包名 + +包名用小写,使用短命名,尽量和标准库不要冲突 + +接口名 + +单个函数的接口名以”er”作为后缀,如Reader,Writer + +接口的实现则去掉“er” + +type Reader interface { + Read(p []byte) (n int, err error) +} + + +两个函数的接口名综合两个函数名 + +type WriteFlusher interface { + Write([]byte) (int, error) + Flush() error +} +三个以上函数的接口名,类似于结构体名 + +type Car interface { + Start([]byte) + Stop() error + Recover() +} + +变量 + +全局变量:采用驼峰命名法,仅限在包内的全局变量,包外引用需要写接口,提供调用 局部变量:驼峰式,小写字母开头 + +常量 + +常量:大写,采用下划线 + +import 规范 +import在多行的情况下,goimports会自动帮你格式化,在一个文件里面引入了一个package,建议采用如下格式: + +import ( + "fmt" +) + +如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包: + +import ( + "encoding/json" + "strings" + + "myproject/models" + "myproject/controller" + "git.obc.im/obc/utils" + + "git.obc.im/dep/beego" + "git.obc.im/dep/mysql" +) + +在项目中不要使用相对路径引入包: + +// 这是不好的导入 +import “../net” + +// 这是正确的做法 +import “xxxx.com/proj/net” + +函数名 + +函数名采用驼峰命名法,尽量不要使用下划线 + +错误处理 + +error作为函数的值返回,必须尽快对error进行处理 +采用独立的错误流进行处理 +不要采用这种方式 + + if err != nil { + // error handling + } else { + // normal code + } + +而要采用下面的方式 + + if err != nil { + // error handling + return // or continue, etc. + } + // normal code + +如果返回值需要初始化,则采用下面的方式 +x, err := f() +if err != nil { + // error handling + return +} +// use x + +Panic + +在逻辑处理中禁用panic +在main包中只有当实在不可运行的情况采用panic,例如文件无法打开,数据库无法连接导致程序无法 正常运行,但是对于其他的package对外的接口不能有panic,只能在包内采用。 建议在main包中使用log.Fatal来记录错误,这样就可以由log来结束程序。 + +Recover +recover用于捕获runtime的异常,禁止滥用recover,在开发测试阶段尽量不要用recover,recover一般放在你认为会有不可预期的异常的地方。 + +func server(workChan <-chan *Work) { + for work := range workChan { + go safelyDo(work) + } +} + +func safelyDo(work *Work) { + defer func() { + if err := recover(); err != nil { + log.Println("work failed:", err) + } + }() + // do 函数可能会有不可预期的异常 + do(work) +} + +Defer +defer在函数return之前执行,对于一些资源的回收用defer是好的,但也禁止滥用defer,defer是需要消耗性能的,所以频繁调用的函数尽量不要使用defer。 + +// Contents returns the file's contents as a string. +func Contents(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() // f.Close will run when we're finished. + + var result []byte + buf := make([]byte, 100) + for { + n, err := f.Read(buf[0:]) + result = append(result, buf[0:n]...) // append is discussed later. + if err != nil { + if err == io.EOF { + break + } + return "", err // f will be closed if we return here. + } + } + return string(result), nil // f will be closed if we return here. +} + +控制结构 +if + +if接受初始化语句,约定如下方式建立局部变量 + +if err := file.Chmod(0664); err != nil { + return err +} +for + +采用短声明建立局部变量 + +sum := 0 +for i := 0; i < 10; i++ { + sum += i +} +range + +如果只需要第一项(key),就丢弃第二个: + +for key := range m { + if key.expired() { + delete(m, key) + } +} +如果只需要第二项,则把第一项置为下划线 + +sum := 0 +for _, value := range array { + sum += value +} +return + +尽早return:一旦有错误发生,马上返回 + +f, err := os.Open(name) +if err != nil { + return err +} +d, err := f.Stat() +if err != nil { + f.Close() + return err +} +codeUsing(f, d) + +方法的接收器 +名称 一般采用strcut的第一个字母且为小写,而不是this,me或者self + + type T struct{} + func (p *T)Get(){} +如果接收者是map,slice或者chan,不要用指针传递 + +//Map +package main + +import ( + "fmt" +) + +type mp map[string]string + +func (m mp) Set(k, v string) { + m[k] = v +} + +func main() { + m := make(mp) + m.Set("k", "v") + fmt.Println(m) +} +//Channel +package main + +import ( + "fmt" +) + +type ch chan interface{} + +func (c ch) Push(i interface{}) { + c <- i +} + +func (c ch) Pop() interface{} { + return <-c +} + +func main() { + c := make(ch, 1) + c.Push("i") + fmt.Println(c.Pop()) +} + +如果需要对slice进行修改,通过返回值的方式重新赋值 + +//Slice +package main + +import ( + "fmt" +) + +type slice []byte + +func main() { + s := make(slice, 0) + s = s.addOne(42) + fmt.Println(s) +} + +func (s slice) addOne(b byte) []byte { + return append(s, b) +} + +如果接收者是含有sync.Mutex或者类似同步字段的结构体,必须使用指针传递避免复制 + +package main + +import ( + "sync" +) + +type T struct { + m sync.Mutex +} + +func (t *T) lock() { + t.m.Lock() +} + +/* +Wrong !!! +func (t T) lock() { + t.m.Lock() +} +*/ + +func main() { + t := new(T) + t.lock() +} + +如果接收者是大的结构体或者数组,使用指针传递会更有效率。 + +package main + +import ( + "fmt" +) + +type T struct { + data [1024]byte +} + +func (t *T) Get() byte { + return t.data[0] +} + +func main() { + t := new(T) + fmt.Println(t.Get()) +} + + + + + +