Hyperledger Fabric智能合约开发和测试

Hyperledger Fabric(HLF)是一个使用分布式分类帐技术(DLT)的开源平台,旨在开发可在由组织团体使用访问规则(允许)创建和控制的商业网络环境中工作的应用程序。


该平台以HLF术语支持智能合约-以通用语言(例如Golang,JavaScript,Java)创建的链代码,与以太坊不同,例如以太坊使用以面向合同的有限功能的Solidity语言(LLL,Viper等)。



由于需要部署大量的区块链网络组件,因此链代码的开发和测试可能是一个相当漫长的过程,需要花费大量时间来测试更改。 本文讨论了一种使用CCKit库快速开发和测试HLF Golang智能合约的方法。


基于HLF的应用


从开发人员的角度来看,区块链应用程序包含两个主要部分:


  • 链上 -在区块链网络的隔离环境中运行的智能合约(程序),用于确定创建规则和交易属性的组成。 在智能合约中,主要动作是从区块链网络状态读取,更新和删除数据。 应该强调的是,从状态中删除数据会留下该数据存在的信息。
  • 脱链是通过SDK与区块链环境进行交互的应用程序(例如,API)。 交互被理解为调用智能合约功能并监视智能合约事件-外部事件可以导致智能合约中的数据更改,而智能合约中的事件可以触发外部系统中的操作。

通常通过“家庭”区块链网络节点读取数据。 为了记录数据,应用程序将请求发送到参与特定智能合约“批准策略”的组织的节点。


为了开发链下代码(API等),使用了专用的SDK,该SDK封装了与区块链节点的交互,收集响应等。 对于HLF,有用于Go( 1,2 ), Node.JsJava的 SDK实现。


Hyperledger面料组件


频道


通道是节点的单独子网,支持独立的区块链(分类帐)以及用于操作智能合约的区块链的当前状态(键值)( 世界状态 )。 主机可以访问任意数量的通道。


交易额


Hyperledger Fabric中的事务是对区块链状态的原子更新,即执行chaincode方法的结果。 事务包括一个调用链码方法的请求,该链代码方法带有一些由调用节点签名的参数(事务提议),以及来自在其上“确认”事务的节点的一组响应(事务提议响应)(认可)。 响应包含有关“ 读写集”块链状态变化的键值对的信息和服务信息(确认交易的节点的签名和证书)。 因为 各个通道的块链在物理上是分开的,因此只能在一个通道的上下文中执行事务。


诸如比特币以太坊之类的“经典” 区块链平台使用由所有节点执行的订购-执行交易周期,这限制了区块链网络的可扩展性。



Hyperledger Fabric使用具有3个主要操作的事务执行和分发架构:


  • 执行( 执行 )-通过在一个或多个网络节点上运行的智能合约进行创建,交易-分布式注册表状态的原子变化( 认可


  • 排序-专门的订购服务使用可插入共识算法将交易排序和分组为块。


  • 验证-由网络节点验证来自订购者的交易,然后再将来自其的信息放入其分布式注册表中




这种方法允许您在交易执行阶段进入区块链网络之前执行它,并水平扩展网络节点的运行。


链码


链码(也可以称为智能合约)是用Golang,JavaScript(HLF 1.1+)或Java(HLF 1.3+)编写的程序,它定义用于创建更改区块链状态的交易的规则。 该程序在区块链节点分布式网络的几个独立节点上同时执行,通过协调交易“确认”所需的所有节点上程序执行的结果,为智能合约的执行创建了中立的环境。


该代码必须实现一个由方法组成的接口:


type Chaincode interface { // Init is called during Instantiate transaction Init(stub ChaincodeStubInterface) pb.Response // Invoke is called to update or query the ledger Invoke(stub ChaincodeStubInterface) pb.Response } 

  • 在实例化或升级代码时调用Init方法。 此方法对代码状态进行必要的初始化。 在方法代码中区分调用是实例化还是升级,这一点很重要,因此,由于错误,您不会初始化(重置)在代码操作过程中已经接收到非零状态的数据。
  • 访问代码代码的任何功能时,将调用Invoke方法。 此方法适用于智能合约的状态。

链码安装在区块链网络的对等点上。 在系统级别,代码的每个实例对应于连接到特定网络节点的单独的docker-container,该节点执行调度调用以执行代码。
与以太坊智能合约不同,可以更新链接逻辑,但是这要求托管代码的所有节点都必须安装更新版本。


响应从外部通过SDK调用chaincode函数,chaincode会更改区块链( Read-Write Set )以及事件的状态。 链码是指一个特定的通道,并且只能在一个通道中更改数据。 同时,如果安装了代码的主机也可以访问其他通道,则代码的逻辑可以从这些通道读取数据。


用于管理区块链网络运行各个方面的特殊链代码称为系统链代码。


背书政策


批准策略在特定链码生成的交易级别定义共识规则。 该策略设置确定哪些通道节点应创建事务的规则。 为此,批准策略中指定的每个节点必须运行链接方法(“执行”步骤),执行“模拟”,之后,签名的结果将由发起交易的SDK收集和验证(所有模拟结果必须相同,该策略要求的所有节点的签名都必须存在)。 接下来,SDK将交易发送给订购者 ,此后,所有有权访问该通道的节点都将通过订购者接收交易,并执行“验证”步骤。 需要强调的是,并非所有通道节点都必须参与“执行”步骤。


批准策略是在实例化或升级代码时确定的。 在1.3版中,不仅可以在链码级别设置策略,而且可以在单个基于状态的认可密钥级别设置策略。 批准政策示例:


  • 节点A,B,C,D
  • 大多数通道节点
  • 来自A,B,C,D,E,F的至少3个节点

大事记


事件是一个命名数据集,可让您发布区块链链状态的“更新提要”。 事件属性集定义了链码。


网络基础设施


主持人(对等)


主机连接到其具有访问权限的任意数量的通道。 主机维护其版本的区块链和区块链的状态,并提供运行链代码的环境。 如果主机不是批准策略的一部分,则不必使用链码进行设置。


在主机软件级别,可以将区块链的当前状态(世界状态)存储在LevelDB或CouchDB中。 CouchDB的优点是它支持使用MongoDB语法的丰富查询。


订购者


事务管理服务接受已签名的事务作为输入,并确保事务以正确的顺序分布在网络节点上。


定购者不运行智能合约,并且不包含区块链和区块链状态。 目前(1.3),有两种订购者实现方式-一个开发单独版本和一个基于Kafka的版本,该版本提供崩溃容错能力。 预计在2018年底将实现支持部分参与者的错误行为(拜占庭容错)的订购者的实施。


身份服务


在Hyperledger Fabric网络中,所有成员均具有其他成员已知的身份(身份)。 为了进行标识,使用了公钥基础结构(PKI),通过该PKI,可以为组织,基础结构元素(节点,订购者),应用程序和最终用户创建X.509证书。 结果,可以通过网络级别,单个通道上或智能合约的逻辑中的访问规则来控制对读取和修改数据的访问。 在同一个区块链网络中,各种类型的多种识别服务可以同时工作。


代码的实现


Chaincode可以被视为具有实现特定业务逻辑的方法的对象。 与经典OOP不同,链码不能具有属性字段。 为了处理状态,该状态的存储由HLF 区块链平台提供,使用ChainchainStubInterface层,该层在调用 InitInvoke方法时传递。 它提供了接收函数调用参数并更改块链状态的功能:


 type ChaincodeStubInterface interface { // GetArgs returns the arguments intended for the chaincode Init and Invoke GetArgs() [][]byte // InvokeChaincode locally calls the specified chaincode InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response // GetState returns the value of the specified `key` from the ledger. GetState(key string) ([]byte, error) // PutState puts the specified `key` and `value` into the transaction's writeset as a data-write proposal. PutState(key string, value []byte) error // DelState records the specified `key` to be deleted in the writeset of the transaction proposal. DelState(key string) error // GetStateByRange returns a range iterator over a set of keys in the ledger. GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) // CreateCompositeKey combines the given `attributes` to form a composite key. CreateCompositeKey(objectType string, attributes []string) (string, error) // GetCreator returns `SignatureHeader.Creator` (eg an identity of the agent (or user) submitting the transaction. GetCreator() ([]byte, error) // and many more methods } 

在基于Solidity开发的以太坊智能合约中,每种方法都具有公共功能。 在Hyperledger Fabric链代码中,使用ChaincodeStubInterface函数的InitInvoke方法。 使用GetArgs(),可以以字节数组的形式获取函数调用的参数,而调用Invoke时数组的第一个元素包含chaincode函数的名称。 因为 任何chaincode方法的调用都通过Invoke方法传递;我们可以说这是前端控制器模式的实现。


例如,如果我们考虑为ERC-20令牌实现标准以太坊接口,则智能合约应实现以下方法:


  • totalSupply()
  • balanceOf(地址_所有者)
  • 转移(地址_to,uint256 _value)

对于HLF实现, Invoke函数代码必须能够处理其中Invoke调用的第一个参数包含所需方法名称的情况(例如,“ totalSupply”或“ balanceOf”)。 在此处可以看到实现ERC-20标准的示例。


链码示例


除了Hyperledger Fabric文档外, 还有一些链代码示例:



在这些示例中,链代码的实现相当冗长,并且包含许多重复的逻辑,用于选择被调用的路由函数),检查参数数量,json编组/解组:


 func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { function, args := stub.GetFunctionAndParameters() fmt.Println("invoke is running " + function) // Handle different functions if function == "initMarble" { //create a new marble return t.initMarble(stub, args) } else if function == "transferMarble" { //change owner of a specific marble return t.transferMarble(stub, args) } else if function == "readMarble" { //read a marble return t.readMarble(stub, args) } else ... 

当您只是忘记解组输入数据时,这样的代码组织会导致代码的可读性下降和可能的错误,例如this 。 关于HLF开发计划的演讲提到了链代码开发方法的修订版,特别是Java链代码中的注释的引入等,但是,该计划与仅在2019年预期的版本相关。 开发智能合约的经验得出的结论是,如果您在单独的库中选择基本功能,则开发和测试链代码将更加容易。


CCKit-用于开发和测试链式代码的库


CCKit库总结了开发和测试链代码的实践。 作为开发链码扩展的一部分,以Ethereum智能合约扩展的 OpenZeppelin库为例。 CCKit使用以下体系结构解决方案:


将呼叫路由到智能合约功能


路由是指应用程序响应客户端请求的算法。 例如,几乎所有的http框架都使用这种方法。 路由器使用某些规则来绑定请求和请求处理程序。 关于链码,这是将链码功能的名称与处理程序功能相关联。


在智能合约的最新示例中,例如在Insurance App中 ,它使用链码函数名称和Golang代码形式的函数之间的映射:


 var bcFunctions = map[string]func(shim.ChaincodeStubInterface, []string) pb.Response{ // Insurance Peer "contract_type_ls": listContractTypes, "contract_type_create": createContractType, ... "theft_claim_process": processTheftClaim, } 

CCKit路由器使用类似于http路由器的方法,并且能够将请求上下文用于链码功能和中间件功能


调用代码的上下文


与通常可以访问http请求参数的http请求上下文类似,CCKit路由器使用对智能合约函数的调用上下文,该函数是shim.ChaincodeStubInterface之上的抽象 。 上下文可以是链接函数处理程序的唯一参数;通过它,处理程序可以接收函数调用的参数,以及访问辅助功能以使用智能合约的状态(State),创建答案(Response)等。


 Context interface { Stub() shim.ChaincodeStubInterface Client() (cid.ClientIdentity, error) Response() Response Logger() *shim.ChaincodeLogger Path() string State() State Time() (time.Time, error) Args() InterfaceMap Arg(string) interface{} ArgString(string) string ArgBytes(string) []byte SetArg(string, interface{}) Get(string) interface{} Set(string, interface{}) SetEvent(string, interface{}) error } 

因为 上下文是一个接口,在某些链式代码中,它可以扩展。


中间件功能


在调用代码代码方法的处理程序之前,将调用中间处理功能(中间件),可以访问对代码方法和下一个中间函数的调用上下文,或者直接访问下一个(下一个)方法的处理程序。 中间件可用于:


  • 转换输入数据(在下面的示例中, p.Stringp.Struct是中间件)
  • 使用该功能的限制(例如owner.Only
  • 请求处理周期的完成
  • 从堆栈中调用下一个中间处理函数

数据结构转换


chaincode接口假定将字节数组的数组提供给输入,其每个元素都是chaincode函数的属性。 为了防止从链接函数的每个处理程序中的函数调用参数将字节组数据手动编组为golang数据类型(int,字符串,结构,数组),在CCKit路由器中创建路由规则时会设置预期的数据类型,并自动转换类型。 在下面的示例中, carGet函数需要一个字符串类型的参数,而carRegister函数需要一个CarPayload结构。 该参数也被命名,它允许处理程序按名称从上下文中获取其值。 下面将给出处理程序的示例。 Protobuf也可以用于描述链接数据方案。


 r.Group(`car`). Query(`List`, cars). // chain code method name is carList Query(`Get`, car, p.String(`id`)). // chain code method name is carGet, method has 1 string argument "id" Invoke(`Register`, carRegister, p.Struct(`car`, &CarPayload{}), // 1 struct argument owner.Only) // allow access to method only for chaincode owner (authority) 

此外,在将数据写入智能合约的状态以及创建事件(golang类型被序列化为字节数组)时,将使用自动转换(编组)。


调试和记录链代码的工具


要调试代码,可以使用debug扩展,该扩展实现了智能合约方法,该方法将允许您检查智能合约状态下是否存在键,以及直接读取/更改/删除键值。


为了在对chaincode函数的调用的上下文中记录日志,可以使用Log()方法,该方法返回HLF中使用的记录器的实例。


智能合约方法访问控制方法


作为所有者扩展的一部分,实现了用于存储有关实例化链代码的所有者和智能合约方法的访问修饰符(中间件)的信息的基本原语。


智能合约测试工具


部署区块链网络,安装和初始化链代码是一个相当复杂的设置,而且过程很长。 通过使用智能合约的DEV模式,可以减少重新安装/升级智能合约代码的时间,但是,更新代码的过程仍然很慢。


shim软件包包含MockStub的实现,该实现包装对代码的调用,以模拟其在HLF区块链环境中的操作。 使用MockStub可以使您几乎立即获得测试结果,并可以减少开发时间。 如果我们考虑HLF中代码的一般操作方案,则MockStub本质上替代了SDK,从而使您可以调用代码的功能,并模拟在主机上启动代码的环境。



HLF交付中的MockStub包含了shim.ChaincodeStubInterface接口的几乎所有方法的实现,但是,在当前版本(1.3)中,它缺少某些重要方法的实现,例如GetCreator。 因为 链代码可以使用此方法来获得用于访问控制的事务创建者的证书,为了最大程度地覆盖测试,具有此方法的存根的能力很重要。


CCKit库包含MockStub的扩展版本,其中包含缺少的方法的实现以及用于事件通道的方法等。


链码示例


例如,让我们创建一个简单的链码来存储有关已注册汽车的信息


资料模型


代码的状态是键值存储,其中键是字符串,值是字节数组。 基本做法是将经过分类处理的golang数据结构实例存储为值。 因此,要使用链码中的数据,从状态读取后,需要解组字节数组。


为了记录有关汽车的信息,我们将使用以下属性集:


  • 标识符(车号)
  • 汽车模型
  • 车主信息
  • 数据变更时间信息

 // Car struct for chaincode state type Car struct { Id string Title string Owner string UpdatedAt time.Time // set by chaincode method } 

要将数据传输到链码,请创建一个单独的结构,该结构仅包含来自链码外部的字段:


 // CarPayload chaincode method argument type CarPayload struct { Id string Title string Owner string } 

使用按键


智能合约状态下的记录键是一个字符串。 它还支持创建复合键的功能,其中组合键的各个部分之间用零字节( U + 0000 )分隔。


 func CreateCompositeKey(objectType string, attributes []string) (string, error) 

CCKit中,如果传输的结构支持Keyer接口,则使用智能合约状态功能可以自动创建记录密钥。


 // Keyer interface for entity containing logic of its key creation type Keyer interface { Key() ([]string, error) } 

要记录汽车,密钥生成功能如下:


 const CarEntity = `CAR` // Key for car entry in chaincode state func (c Car) Key() ([]string, error) { return []string{CarEntity, c.Id}, nil } 

智能合约功能声明(路由)


在链码的构造方法中,我们可以定义链码的功能及其参数。 汽车登记代码中将包含3个功能


  • carList,返回一个Car结构数组
  • carGet,接受汽车标识符并返回汽车结构
  • carRegister,接受CarPayload结构的序列化实例,并返回注册结果。 仅链代码的所有者可以访问此方法,链代码的所有者使用中间件从所有者包中保存该代码

 func New() *router.Chaincode { r := router.New(`cars`) // also initialized logger with "cars" prefix r.Init(invokeInit) r.Group(`car`). Query(`List`, queryCars). // chain code method name is carList Query(`Get`, queryCar, p.String(`id`)). // chain code method name is carGet, method has 1 string argument "id" Invoke(`Register`, invokeCarRegister, p.Struct(`car`, &CarPayload{}), // 1 struct argument owner.Only) // allow access to method only for chaincode owner (authority) return router.NewChaincode(r) } 

上面的示例使用Chaincode结构,其中将InitInvoke方法的处理委托给路由器:


 package router import ( "github.com/hyperledger/fabric/core/chaincode/shim" "github.com/hyperledger/fabric/protos/peer" ) // Chaincode default chaincode implementation with router type Chaincode struct { router *Group } // NewChaincode new default chaincode implementation func NewChaincode(r *Group) *Chaincode { return &Chaincode{r} } //======== Base methods ==================================== // // Init initializes chain code - sets chaincode "owner" func (cc *Chaincode) Init(stub shim.ChaincodeStubInterface) peer.Response { // delegate handling to router return cc.router.HandleInit(stub) } // Invoke - entry point for chain code invocations func (cc *Chaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response { // delegate handling to router return cc.router.Handle(stub) } 

使用路由器和基本的Chaincode结构可以重用处理程序功能。 例如,要在不检查对carRegister函数的访问权限的情况下实现链carRegister ,只需创建一个新的构造方法即可


实施智能合约的功能


Golang函数-CCKit路由器中的智能合约函数处理程序可以分为三种类型:


  • StubHandlerFunc-标准处理程序接口,接受shim.ChaincodeStubInterface ,返回标准响应对等体。
  • ContextHandlerFunc-获取上下文并返回对等
  • HandlerFunc-获取上下文,返回接口和错误。 可以返回字节数组,也可以根据创建的对等方自动将任何golang类型转换为字节数组。 响应状态将为shim.Okshim.Error ,具体取决于传递的错误。

 // StubHandlerFunc acts as raw chaincode invoke method, accepts stub and returns peer.Response StubHandlerFunc func(shim.ChaincodeStubInterface) peer.Response // ContextHandlerFunc use stub context as input parameter ContextHandlerFunc func(Context) peer.Response // HandlerFunc returns result as interface and error, this is converted to peer.Response via response.Create HandlerFunc func(Context) (interface{}, error) 

, , ( CarPayload)
State , ( )


 // car get info chaincode method handler func car(c router.Context) (interface{}, error) { return c.State().Get( // get state entry Key(c.ArgString(`id`)), // by composite key using CarKeyPrefix and car.Id &Car{}) // and unmarshal from []byte to Car struct } // cars car list chaincode method handler func cars(c router.Context) (interface{}, error) { return c.State().List( CarKeyPrefix, // get list of state entries of type CarKeyPrefix &Car{}) // unmarshal from []byte and append to []Car slice } // carRegister car register chaincode method handler func carRegister(c router.Context) (interface{}, error) { // arg name defined in router method definition p := c.Arg(`car`).(CarPayload) t, _ := c.Time() // tx time car := &Car{ // data for chaincode state Id: p.Id, Title: p.Title, Owner: p.Owner, UpdatedAt: t, } return car, // peer.Response payload will be json serialized car data c.State().Insert( //put json serialized data to state Key(car.Id), // create composite key using CarKeyPrefix and car.Id car) } 

-


- — , . BDD – Behavior Driven Development, .


, , - Ethereum ganache-cli truffle . golang - Mockstub.



, . .


Ginkgo , Go, go test . gomega expect , , .


  import ( "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" examplecert "github.com/s7techlab/cckit/examples/cert" "github.com/s7techlab/cckit/extensions/owner" "github.com/s7techlab/cckit/identity" "github.com/s7techlab/cckit/state" testcc "github.com/s7techlab/cckit/testing" expectcc "github.com/s7techlab/cckit/testing/expect" ) func TestCars(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Cars Suite") } 

, CarPayload :


 var Payloads = []*Car{{ Id: `A777MP77`, Title: `VAZ`, Owner: `victor`, }, { Id: `O888OO77`, Title: `YOMOBIL`, Owner: `alexander`, }, { Id: `O222OO177`, Title: `Lambo`, Owner: `hodl`, }} 

MockStub Cars.


 //Create chaincode mock cc := testcc.NewMockStub(`cars`, New()) 

因为 cars , .


 // load actor certificates actors, err := identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{ `authority`: `s7techlab.pem`, `someone`: `victor-nosov.pem`}, examplecert.Content) 

BeforeSuite Car authority Init . , Cars Init Init , .


 BeforeSuite(func() { // init chaincode expectcc.ResponseOk(cc.From(actors[`authority`]).Init()) // init chaincode from authority }) 

. , CarRegister , .


 It("Allow authority to add information about car", func() { //invoke chaincode method from authority actor expectcc.ResponseOk(cc.From(actors[`authority`]).Invoke(`carRegister`, Payloads[0])) }) It("Disallow non authority to add information about car", func() { //invoke chaincode method from non authority actor expectcc.ResponseError( cc.From(actors[`someone`]).Invoke(`carRegister`, Payloads[0]), owner.ErrOwnerOnly) // expect "only owner" error }) 

:


 It("Disallow authority to add duplicate information about car", func() { expectcc.ResponseError( cc.From(actors[`authority`]).Invoke(`carRegister`, Payloads[0]), state.ErrKeyAlreadyExists) //expect car id already exists }) 

结论


- HLF Go, Java, JavaScript, , , - (Solidity) / -. / .


HLF , , ( .). Hypeledger Fabric , .. .

Source: https://habr.com/ru/post/zh-CN426705/


All Articles