Hyperledger Fabric Smart Contract Development and Testing
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 .
HLF based application
From the point of view of the developer, the blockchain application consists of two main parts:
- On-chain - smart contracts (programs) operating in an isolated environment of the blockchain network, defining the rules for creating and composition of transaction attributes. The smart contract basic steps - is read, update and delete data from the state ( state ) blokcheyn network. It should be emphasized that deleting data from the state leaves information that this data was present.
- Off-chain is an application (for example, API) that interacts with the blockchain environment via the SDK. Interaction means calling the functions of smart contracts and observing the events of a smart contract — external events can trigger a change in data in a smart contract, while events in a smart contract can trigger actions in external systems.
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
Hyperledger Fabric Components
Channel
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.
Transaction
A transaction in Hyperledger Fabric is an atomic update of the state of a 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 )
Sequencing ( order ) - ordering and grouping of transactions in units specialized service orderer using plug (pluggable) consensus algorithm.
Check ( the validate ) - verification of network nodes received from the orderer transactions before posting information from them in their copies distributed registry
Such an approach allows to carry out the transaction execution stage before it enters the blockchain network, as well as horizontally scale the operation of network nodes.
Cheinkode
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 Init method is invoked during the installation or upgrade of a chejncode. In this method, the necessary initialization of the state of the chynode is performed. It is important to distinguish in the method code whether the call is instantiation or upgrade, so as not to initialize (reset) the data by mistake, which in the course of the work of the cheinkcode received a non-zero state.
- The Invoke method is called when calling any function of the cheyncode. In this method, we work with the state of smart contracts.
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.
Endorsement Policy
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. Important to emphasize
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:
- Nodes A, B, C, D
- Most channel nodes
- At least 3 nodes from A, B, C, D, E, F
Event
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.
Network infrastructure
Network node (Peer)
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.
Orderer
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.
Membership services
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 the same blockchain network, several identification services of various types can operate simultaneously.
Chejncode implementation
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 the 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` (e.g. 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:
- totalSupply ()
- balanceOf (address _owner)
- transfer (address _to, uint256 _value)
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 .
Examples of cheynkodov
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 functionsif function == "initMarble" { //create a new marblereturn t.initMarble(stub, args)
} elseif function == "transferMarble" { //change owner of a specific marblereturn t.transferMarble(stub, args)
} elseif function == "readMarble" { //read a marblereturn 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.
CCKit - library for development and testing of cheynkodov
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 (routing) calls to the functions of the smart contract
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)
The context of the appeal to the function cheynkoda
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.
Middleware functions (middleware)
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:
- input data conversion (in the example below p.String and p.Struct are middleware)
- function access restrictions (for example, owner.Only )
- complete the request processing cycle
- calling the next staging function from the stack
Data structure conversion
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 described later, the function carGet to expect an argument of type string, and the function carRegister structure CarPayload . 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)
Means of debugging and logging of cheynkodov
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.
Control methods for accessing smart contract methods
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.
Smart Contract Testing Tools
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.
Cheynkod example
For an example, we will create a simple cheinkcode for storing information about registered cars.
Data model
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 byte array.
To write about the car will use the following set of attributes:
- Identifier (car number)
- Car model
- Vehicle Owner Information
- Information about data modification time
// Car struct for chaincode statetype 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 argumenttype CarPayload struct {
Id string
Title string
Owner string
}
Work with keys
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 )
funcCreateCompositeKey(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 creationtype 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 statefunc(c Car)Key()([]string, error) {
return []string{CarEntity, c.Id}, nil
}
Declaration of smart contract functions (routing)
In the cheyncode constructor method, we can define cheyncode functions and their arguments. There will be 3 functions in the cheynkod car registration
- carList, returns an array of Car structures
- carGet, takes the car ID and returns the Car structure
- carRegister, accepts a serialized instance of the CarPayload structure and returns the registration result. Access to this method is possible only for the owner of the cheyncode, which is saved using middleware from the owner package.
funcNew() *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 routertype Chaincode struct {
router *Group
}
// NewChaincode new default chaincode implementationfuncNewChaincode(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 routerreturn cc.router.HandleInit(stub)
}
// Invoke - entry point for chain code invocationsfunc(cc *Chaincode)Invoke(stub shim.ChaincodeStubInterface)peer.Response {
// delegate handling to routerreturn 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 function carRegister
will be enough to create a new constructor method
Implementing Smart Contract Functions
Golang functions - smart contract function handlers in CCKit router can be of three types:
- StubHandlerFunc - the standard handler interface, accepts shim.ChaincodeStubInterface , returns the standard response peer.Response
- ContextHandlerFunc - takes a context and returns peer.Response
- HandlerFunc - accepts context, returns interface and error. It can return an array of bytes or any type of golang that is automatically converted into an array of bytes, on the basis of which peer.Response is created . The response status will be shim.Ok or shim.Error , depending on the error transmitted.
// StubHandlerFunc acts as raw chaincode invoke method, accepts stub and returns peer.Response
StubHandlerFunc func(shim.ChaincodeStubInterface)peer.Response
// ContextHandlerFuncusestubcontextasinputparameterContextHandlerFuncfunc(Context)peer.Response
// HandlerFuncreturnsresultasinterfaceanderror, thisisconvertedtopeer.Responseviaresponse.CreateHandlerFuncfunc(Context)(interface{}, error)
The arguments of the cheyncode functions described in the router will be automatically converted from arrays of bytes to the target data types (string or CarPayload structure).
The cheyncode function uses State methods that simplify extraction and saving data to the cheyncode by automatically creating keys and converting the transmitted data into arrays. byte (in the state of the cheynkod is written an array of bytes)
// car get info chaincode method handlerfunccar(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 handlerfunccars(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 handlerfunccarRegister(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)
}
Smart contract tests
The general principle of testing smart contracts is similar to testing any other code — a set of reference method calls in a predefined environment is created, for the results of which statements are written. For testing, it is convenient to use the practices of BDD - Behavior Driven Development, which, along with tests, allow you to create documentation and examples of use.
For testing, for example, Ethereum smart contracts can use the emulator nodes ganache-cli and framework truffle . For testing golang smart contracts, Mockstub is enough.
Sample test
In the example, we will create a test that checks the correctness of the work of the cheyncode functions. A full test sample is available here .
The test code uses the Ginkgo library , which extends the Go test infrastructure, which allows the use of a standard command go test
. The tests will also use the gomega package to create general statements and the expect package , which contains statements used in working with cheynkodami.
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"
)
funcTestCars(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cars Suite")
}
The tests will also use fixtures containing examples of CarPayload :
var Payloads = []*Car{{
Id: `A777MP77`,
Title: `VAZ`,
Owner: `victor`,
}, {
Id: `O888OO77`,
Title: `YOMOBIL`,
Owner: `alexander`,
}, {
Id: `O222OO177`,
Title: `Lambo`,
Owner: `hodl`,
}}
Next, you need to create an instance of MockStub with the Cars chink code.
//Create chaincode mock
cc := testcc.NewMockStub(`cars`, New())
Since in the cars' cheynkod, information about the certificate that creates the transaction is used, then we load test certificates .
// load actor certificates
actors, err := identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{
`authority`: `s7techlab.pem`,
`someone`: `victor-nosov.pem`}, examplecert.Content)
In the BeforeSuite function , we initialize the Car cheincode on behalf of the certificate under the authority code and we expect that the Cheyncode Init method will return a successful result. It should be noted that in cheynkode Cars in the Init method in the state cheynkoda recorded details of the calling Init , it is considered the owner of cheynkoda.
BeforeSuite(func() {
// init chaincode
expectcc.ResponseOk(cc.From(actors[`authority`]).Init()) // init chaincode from authority
})
Next, we call the cheynkod function and compare the answers with the reference. For example, we can check that the owner of the cheynkod can call the CarRegister method , at the same time, when trying to call the cheyncode function from another certificate, the error should return.
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
})
We can also make sure that when trying to enter duplicate information there will also be an 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
})
Conclusion
HLF smart contracts offer development in such programming languages as Go, Java, JavaScript, which, in comparison with specialized contract-oriented languages (Solidity), allows using existing frameworks / libraries in smart contracts. It is also technically possible to integrate our own developments into the development / testing approach of the cheyncode.
The cheynkodov architecture in HLF is actively being developed, there are functions that were clearly not enough before (page list request for records, etc.). Hypeledger Fabric's contributors are actively urging interested developers to join in the development of the project, because The field for development is quite large.