Hyperledger Fabric (HLF) is an open source platform using distributed ledger technology (DLT) designed to develop applications that work in the business network environment created and controlled by a consortium of organizations using access rules (permissioned).
The platform supports smart contracts, in terms of HLF - chaincode (chaincode) created in general-purpose languages ​​such as Golang, JavaScript, Java, in contrast to, for example, Ethereum, which uses a Solidity contract-oriented, functional language. (LLL, Viper and others).
Development and testing of circuit codes, due to the need to deploy a significant number of components of the blockchain network, can be quite a long process with high time costs for testing changes. The article discusses the approach to the rapid development and testing of HLF smart contracts at Golang using the CCKit library.
From the point of view of the developer, the blockchain application consists of two main parts:
Data is usually read through the “home” node of the blockchain network. To write data, the application sends requests to the nodes of organizations participating in the “approval policy” of a particular smart contract.
To develop off-chain code (API, etc.), a specialized SDK is used, which encapsulates interaction with blockchain nodes, collecting responses, etc. For HLF there are SDK implementations on Go ( 1 , 2 ), Node.Js and Java
A channel is a separate subnet of nodes supporting an isolated block chain (ledger), as well as the current state (key-value) of the block chain ( world state ) used for the operation of smart contracts. A network node can have access to an arbitrary number of channels.
A transaction in the Hyperledger Fabric is an atomic update of the state of the chain of blocks, the result of the execution of the cheyncode method. A transaction consists of a request to call a cheyncode method with some arguments (Transaction Proposal), signed by the calling node, and a set of responses (Transaction Proposal Response) from the nodes on which the "Endorsement" of the transaction was performed. Answers contain information on changing pairs of key-value status of the chain of blocks Read-Write Set and service information (signatures and certificates of nodes that confirmed the transaction). Since chains of blocks of individual channels are physically separated, the transaction can be performed only in the context of one channel.
"Classic" blockchain platforms, such as Bitcoin and Ethereum , use the Sorting-Execution transaction execution cycle performed by all nodes, which limits the scalability of the blockchain network.
Hyperledger Fabric uses a transaction execution and distribution architecture, in which there are 3 basic operations:
Execute ( execute ) - creating a smart contract running on one or several network nodes; a transaction — atomic changing the state of a distributed registry ( endorsement )
Ordering — ordering and grouping transactions into blocks by a specialized orderer service using a pluggable consensus algorithm.
Validation - validation by network nodes of transactions coming from orderer before placing information from them in their copy of the distributed registry
This approach allows you to carry out the transaction execution stage before it enters the blockchain network, as well as horizontally scale the operation of network nodes.
A cheinkode, which can also be called a smart contract, is a program written in Golang, JavaScript (HLF 1.1+) or Java (HLF 1.3+), which defines the rules for creating transactions that change the state of a chain of blocks. The program is executed simultaneously on several independent nodes of the blockchain-distributed network of nodes, creating a neutral environment for executing smart contracts by verifying the results of program execution on all the nodes necessary to "confirm" the transaction.
A cheinkode should implement an interface consisting of methods:
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 }
The cheyncode is installed on the nodes (peer) of the blockchain network. At the system level, each instance of a cheyncode corresponds to a separate docker-container tied to a specific network node that dispatches calls to execute cheyncode.
Unlike Ethereum smart contracts, the logic of the cheyncode can be updated, but this requires that all nodes that host the cheyncode install the updated version.
In response to an outside call to the cheyncode function via the SDK, the cheyncode creates a change in the state of the block chain ( Read-Write Set ), as well as events. A cheinkode refers to a specific channel and can change data in only one channel. At the same time, if the node of the network on which the cheyncode is installed also has access to other channels, in the cheyncode logic there may be reading data from these channels.
Special chain codes for managing various aspects of the blockchain network are called system chain codes.
An approval policy defines consensus rules at the level of transactions created by a particular cheyncode. The policy defines rules that define which channel nodes should create a transaction. To do this, each of the nodes specified in the approval policy must run a chejncode method (step "Execute"), perform a "simulation", after which the signed results will be collected and checked by the SDK that initiated the transaction (all simulation results must be identical, Signatures must be present for all required nodes. Next, the SDK sends the transaction to the orderer , after which all nodes that have access to the channel, through the orderer, will receive the transaction and perform the "Validate" step. It is important to emphasize that not all channel nodes should participate in the "Execute" step.
The approval policy is determined at the time of instantiation (instantiate) or upgrade (upgrade) of the cheyncode. In version 1.3, it became possible to set policies not only at the level of a cheyncode, but also at the level of individual state keys of a chain of blocks ( state based endorsement ). Examples of approval policies:
An event is a named dataset that allows you to publish “update tape” of the state of the blockchain chain. A set of event attributes defines a cheynkod.
The network node is connected to an arbitrary number of channels to which it has access rights. The network node maintains its version of the block chain and the state of the block chain, and also provides an environment for starting the chain codes. If the network node is not included in the approval policy, then it does not have to be installed cheynkodov.
At the software level of the network node, the current state of the world block chain can be stored in LevelDB or in CouchDB. The advantage of CouchDB is support for extended queries (rich query) using MongoDB syntax.
The transaction organizing service accepts signed transactions as input and ensures that transactions are distributed to network nodes in the correct order.
Orderer does not launch smart contracts and does not contain a chain of blocks and the state of a chain of blocks. At the moment (1.3) there are two orderer implementations - a solo for development and a version based on Kafka that provides crash fault tolerance. An implementation of an orderer that supports resistance to the incorrect behavior of a certain percentage of participants (Byzantine fault tolerance) is expected at the end of 2018.
In the Hyperledger Fabric network, all participants have identity details known to other participants. The identification uses a public key infrastructure (PKI), which is used to create X.509 certificates for organizations, infrastructure elements (node, orderer), applications, and end users. As a result, read and modify data access can be controlled through access rules at the network level, individual channel, or in the logic of a smart contract. In one blockchain network, several identification services of various types can simultaneously operate.
A cheynkod can be considered as an object having methods that implement certain business logic. Unlike classical OOP, a cheynkod can not have fields - attributes. To work with the state ( state ), which storage is provided by the HLF blockchain platform, the ChaincodeStubInterface layer is used , which is transmitted when calling the Init and Invoke methods. It provides the ability to get arguments of the function call and make changes in the state of the chain of blocks:
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 }
In the Ethereum smart contract developed by Solidity, each method corresponds to a public function. In the cheyncode Hyperledger Fabric in the Init and Invoke methods using the ChaincodeStubInterface function. GetArgs () you can get the arguments of the function call in the form of an array of arrays of bytes, with the first element of the array when you call Invoke contains the name of the function cheynkod. Since Invoke method passes through the method of any cheynkod method, we can say that this is the implementation of the front controller pattern.
For example, if we consider the implementation of the standard for Ethereum interface token ERC-20 smart contract must implement the methods:
and others. In the case of HLF implementations, the cheyncode in the Invoke function should be able to handle cases where the first argument of the Invoke call contains the name of the expected methods (for example, “totalSupply” or “balanceOf”). An example of the implementation of the standard ERC-20 can be seen here .
In addition to the documentation Hyperledger Fabric, you can give a few examples of cheynkodov:
The implementation of cheynkodov in these examples is quite verbose and contains a lot of repeating logic of choosing called functions “routing”), checking the number of arguments, json marshalling / unmarshalling:
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 ...
Such an organization of the code leads to a deterioration in the readability of the code and possible errors, like this , when you simply forgot to conduct an unmarshalling of the input data. In presentations on HLF development plans, there is a mention of a redesigning of the cheyncode development approach, in particular, the introduction of annotations into java cheinkode, etc., but the plans refer to the version that is expected only in 2019. The experience of developing smart contracts led to the conclusion that it would be easier to develop and test circuit codes if you select the basic functionality in a separate library.
The CCKit library summarizes the practice of developing and testing cheek codes. As part of the development of cheyncode extensions, the OpenZeppelin extension library for Ethereum smart contracts was used as an example. CCKit uses the following architectural solutions:
Routing is an algorithm by which an application responds to a client request. This approach is used, for example, in almost all http-frameworks. A router uses specific rules to associate a request and a request handler. With reference to a cheynkod - this is the link between the name of the cheyncode function and the handler function.
In the last examples of smart contracts, for example in the Insurance App , a mapping is used between the name of the cheyncode function and the function in the Golang code like:
var bcFunctions = map[string]func(shim.ChaincodeStubInterface, []string) pb.Response{ // Insurance Peer "contract_type_ls": listContractTypes, "contract_type_create": createContractType, ... "theft_claim_process": processTheftClaim, }
The CCKit router has a similar approach to the http router, and also added the ability to use the context of the query to the cheyncode function and intermediate processing functions (middleware)
Similar to the http request context, which usually has access to the http request parameters, the CCKit router uses the context of accessing the smart contract function, which is an abstraction on top of shim.ChaincodeStubInterface . The context can be the only argument of the handler of the cheyncode function, through it the handler can receive the arguments of the function call, as well as access to the auxiliary functionalities of working with the state of the smart contract (State), creating responses (Response), etc.
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 }
Since Context is an interface, it can be extended in certain cheynkodakh.
The middleware functions are called before calling the handler of the cheyncode method, they have access to the context of the call to the cheyncode method and to the next intermediate function or directly to the handler of the cheyncode method (next). Middleware can be used for:
The cheinkcode interface assumes that an array of bytes is supplied to the input, each of the elements of which is an attribute of the cheyncode function. In order for each handler of the cheynkod function not to perform manual unmarshalling of data from the byte array into the golang data type (int, string, structure, array) of function call arguments, in the CCKit router, the expected data types are set at the moment of creating the routing rule and the type is automatically converted . In the example that is discussed later, the carGet function expects a string argument, and the carRegister function expects a CarPayload structure. The argument is also named, which allows the handler to get its value from the context by name. An example of the handler will be given below.
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)
Also, automatic conversion (marshalling) is used when writing data to the smart contract state and when creating events (the golang type is serialized into an array of bytes)
To debug a cheyncode, you can use the debug extension, which implements smart contract methods that will allow you to inspect the presence of keys in a smart contract state, as well as directly read / modify / delete the value by key.
For logging, in the context of a call to a cheyncode function, the Log () method can be used, which returns an instance of the logger used in the HLF.
As part of the owner extension, basic primitives are implemented for storing information about the owner of an instantiated cheyncode and access modifiers (middleware) for smart contract methods.
Deploying the network blockchain, installing and initializing cheynkodov is quite a complicated setup and a long procedure. The time to re-install / upgrade the code of a smart contract can be reduced by using the DEV mode of the smart contract, however, the process of updating the code will still be slow.
The shim package contains a MockStub implementation that wraps calls to a cheyncode code, simulating its operation in the blockchain HLF environment. Using MockStub allows you to get test results almost instantly and allows you to reduce development time. If we consider the general scheme of the work of the cheyncode in the HLF, MockStub essentially replaces the SDK, allowing you to make calls to the cheyncode functions, and simulates the environment of starting the cheyncode on the network node.
The HLF MockStub contains implementation of almost all the methods of the shim.ChaincodeStubInterface interface, but in the current version (1.3), it lacks the implementation of some important methods, such as GetCreator. Since The cheynkod can use this method to obtain the certificate of the transaction creator for the purpose of access control, for the maximum coverage in tests it is important to have a stub for this method.
The CCKit library contains an enhanced version of MockStub , which contains the implementation of missing methods, as well as methods for working with event channels, etc.
For an example, we will create a simple cheinkcode for storing information about registered cars.
The state of the cheyncode is the key-value storage, in which the key is a string, the value is an array of bytes. The base practice is to store golang instances of data structures serialized in json as a value. Accordingly, to work with the data in the cheyncode, after reading from the state, it is necessary to conduct an unmarshalling of the byte array.
To write about the car will use the following set of attributes:
// Car struct for chaincode state type Car struct { Id string Title string Owner string UpdatedAt time.Time // set by chaincode method }
To transfer data to the cheyncode, we will create a separate structure containing only the fields coming from outside the cheyncode:
// CarPayload chaincode method argument type CarPayload struct { Id string Title string Owner string }
Record keys in the smart contract state is a string. It also supports the ability to create composite keys in which parts of the key are separated by a zero byte ( U + 0000 )
func CreateCompositeKey(objectType string, attributes []string) (string, error)
In CCKit, smart contract state functions can automatically create keys for entries if the transferred structures support the Keyer interface.
// Keyer interface for entity containing logic of its key creation type Keyer interface { Key() ([]string, error) }
For a vehicle entry, the key creation function will be as follows:
const CarEntity = `CAR` // Key for car entry in chaincode state func (c Car) Key() ([]string, error) { return []string{CarEntity, c.Id}, nil }
In the cheyncode constructor method, we can define cheyncode functions and their arguments. There will be 3 functions in the cheynkod car registration
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) }
In the example above, a Chaincode structure is used in which the processing of the Init and Invoke methods is delegated to the router:
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) }
Using the router and the basic structure of Chaincode allows you to reuse handler functions. For example, to implement a cheynkod without checking access to the carRegister
function will be enough to create a new constructor method
Golang functions - smart contract function handlers in CCKit router can be of three types:
// 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)
The arguments of the cheyncode functions described in the router will be automatically converted from arrays of bytes into the target data types (string or CarPayload structure)
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())
// 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/426705/
All Articles