Skip to content
This repository was archived by the owner on Nov 8, 2023. It is now read-only.

Repository files navigation

Hyperledger Fabric chaincode kit (CCKit)

We're pleased to announce that ССKit has officially joined https://github.com/hyperledger-labs.

CCKit development continues here https://github.com/hyperledger-labs/cckit




Go Report CardBuildCoverage Status

Overview

A smart contract is code, invoked by a client application external to the blockchain network – that manages access and modifications to a set of key-value pairs in the World State. In Hyperledger Fabric, smart contracts are referred to as chaincode.

CCKit is a programming toolkit for developing and testing Hyperledger Fabric golang chaincodes. It enhances the development experience while providing developers components for creating more readable and secure smart contracts.

Chaincode examples

There are several chaincode "official" examples available:

and others

Main problems with existing examples are:

  • Working with chaincode state at very low level
  • Lots of code duplication (JSON marshalling / unmarshalling, validation, access control, etc)
  • Chaincode methods routing appeared only in HLF 1.4 and only in Node.Js chaincode
  • Uncompleted testing tools (MockStub)

CCKit features

Publications with usage examples

Examples based on CCKit

Installation

CCKit requires Go 1.11+ with modules support

Standalone

git clone git@github.com:s7techlab/cckit.git

go mod vendor

As dependency

go get -u github.com/s7techlab/cckit

Example - Commercial Paper chaincode

Scenario

Commercial paper scenario from official documentation describes a Hyperledger Fabric network, aimed to issue, buy and redeem commercial paper.

commercial paper network

5 steps to develop chaincode

Chaincode is a domain specific program which relates to specific business process. The job of a smart contract developer is to take an existing business process and express it as a smart contract in a programming language. Steps of chaincode development:

  1. Define chaincode model - schema for state entries, transaction payload and events
  2. Define chaincode interface
  3. Implement chaincode instantiate method
  4. Implement chaincode methods with business logic
  5. Create tests

Define chaincode model

With protocol buffers, you write a .proto description of the data structure you wish to store. From that, the protocol buffer compiler creates a golang struct that implements automatic encoding and parsing of the protocol buffer data with an efficient binary format (or json).

Code generation can be simplified with a short Makefile:

.: generate
generate:
@echo "schema"
@protoc -I=./ --go_out=./ ./*.proto

Chaincode state

The following file shows how to define the world state schema using protobuf.

examples/cpaper_extended/schema/state.proto

syntax="proto3";
packagecckit.examples.cpaper_extended.schema;
optiongo_package="schema";
import"google/protobuf/timestamp.proto";
// Commercial Paper state entrymessageCommercialPaper {
enumState {
ISSUED=0;
TRADING=1;
REDEEMED=2;
}
// Issuer and Paper number comprises composite primary key of Commercial paper entrystringissuer=1;
stringpaper_number=2;
stringowner=3;
google.protobuf.Timestampissue_date=4;
google.protobuf.Timestampmaturity_date=5;
int32face_value=6;
Statestate=7;
// Additional unique field for entrystringexternal_id=8;
}
// CommercialPaperId identifier partmessageCommercialPaperId {
stringissuer=1;
stringpaper_number=2;
}
// Container for returning multiple entitiesmessageCommercialPaperList {
repeatedCommercialPaperitems=1;
}

Chaincode transaction and events payload

This file defines the data payload used in business logic methods. In this example transaction and event payloads are exactly the same for the sake of brevity, but you could create a different schema for each type of payload.

examples/cpaper_extended/schema/payload.proto

// IssueCommercialPaper eventsyntax="proto3";
packagecckit.examples.cpaper_extended.schema;
optiongo_package="schema";
import"google/protobuf/timestamp.proto";
import"github.com/mwitkow/go-proto-validators/validator.proto";
// IssueCommercialPaper eventmessageIssueCommercialPaper {
stringissuer=1;
stringpaper_number=2;
google.protobuf.Timestampissue_date=3;
google.protobuf.Timestampmaturity_date=4;
int32face_value=5;
// external_id - another unique constraintstringexternal_id=6;
}
// BuyCommercialPaper eventmessageBuyCommercialPaper {
stringissuer=1;
stringpaper_number=2;
stringcurrent_owner=3;
stringnew_owner=4;
int32price=5;
google.protobuf.Timestamppurchase_date=6;
}
// RedeemCommercialPaper eventmessageRedeemCommercialPaper {
stringissuer=1;
stringpaper_number=2;
stringredeeming_owner=3;
google.protobuf.Timestampredeem_date=4;
}

Define chaincode interface

In examples/cpaper_extended/chaincode.go file we will define the mappings, chaincode initialization method and business logic in the transaction methods. For brevity, we will only display snippets of the code here, please refer to the original file for full example.

Firstly we define mapping rules. These specify the struct used to hold a specific chaincode state, it's primary key, list mapping, unique keys, etc. Then we define the schemas used for emitting events.

var (
// State mappingsStateMappings= m.StateMappings{}.
// Create mapping for Commercial Paper entityAdd(&schema.CommercialPaper{},
// Key namespace will be <"CommercialPaper", Issuer, PaperNumber>m.PKeySchema(&schema.CommercialPaperId{}),
// Structure of result for List methodm.List(&schema.CommercialPaperList{}),
// External Id is uniquem.UniqKey("ExternalId"),
)
// EventMappingsEventMappings= m.EventMappings{}.
// Event name will be "IssueCommercialPaper", payload - same as issue payloadAdd(&schema.IssueCommercialPaper{}).
// Event name will be "BuyCommercialPaper"Add(&schema.BuyCommercialPaper{}).
// Event name will be "RedeemCommercialPaper"Add(&schema.RedeemCommercialPaper{})
)

CCKit uses router to define rules about how to map chaincode invocation to a particular handler, as well as what kind of middleware needs to be used during a request, for example how to convert incoming argument from []byte to target type (string, struct, etc).

funcNewCC() *router.Chaincode {
r:=router.New(`commercial_paper`)
// Mappings for chaincode stater.Use(m.MapStates(StateMappings))
// Mappings for chaincode eventsr.Use(m.MapEvents(EventMappings))
// Store in chaincode state information about chaincode first instantiatorr.Init(owner.InvokeSetFromCreator)
// Method for debug chaincode statedebug.AddHandlers(r, `debug`, owner.Only)
r.
// read methodsQuery(`list`, cpaperList).
Query(`get`, cpaperGet, defparam.Proto(&schema.CommercialPaperId{})).
// txn methodsInvoke(`issue`, cpaperIssue, defparam.Proto(&schema.IssueCommercialPaper{})).
Invoke(`buy`, cpaperBuy, defparam.Proto(&schema.BuyCommercialPaper{})).
Invoke(`redeem`, cpaperRedeem, defparam.Proto(&schema.RedeemCommercialPaper{})).
Invoke(`delete`, cpaperDelete, defparam.Proto(&schema.CommercialPaperId{}))
returnrouter.NewChaincode(r)
}

Implement chaincode init method

In many cases during chaincode instantiation we need to define permissions for chaincode functions - "who is allowed to do this thing", incredibly important in the world of smart contracts. The most common and basic form of access control is the concept of ownership: there's one account (combination of MSP and certificate identifiers) that is the owner and can do administrative tasks on contracts. This approach is perfectly reasonable for contracts that only have a single administrative user.

CCKit provides owner extension for implementing ownership and access control in Hyperledger Fabric chaincodes. In the previous snippet, as an init method, we used owner.InvokeSetFromCreator, storing information which stores the information about who is the owner into the world state upon chaincode instantiation.

Implement business rules as chaincode methods

Now we have to define the actual business logic which will modify the world state when a transaction occurs. In this example we will show only the buy method for brevity. Please refer to examples/cpaper_extended/chaincode.go for full implementation.

funcinvokeCPaperBuy(c router.Context) (interface{}, error) {
var (
cpaper*schema.CommercialPaper// Buy transaction payloadbuyData=c.Param().(*schema.BuyCommercialPaper)
// Get the current commercial paper statecp, err=c.State().Get(
&schema.CommercialPaperId{Issuer: buyData.Issuer, PaperNumber: buyData.PaperNumber},
&schema.CommercialPaper{})
)
iferr!=nil {
returnnil, errors.Wrap(err, "not found")
}
cpaper=cp.(*schema.CommercialPaper)
// Validate current ownerifcpaper.Owner!=buyData.CurrentOwner {
returnnil, fmt.Errorf(
"paper %s %s is not owned by %s",
cpaper.Issuer, cpaper.PaperNumber, buyData.CurrentOwner)
}
// First buyData moves state from ISSUED to TRADINGifcpaper.State==schema.CommercialPaper_ISSUED {
cpaper.State=schema.CommercialPaper_TRADING
}
// Check paper is not already REDEEMEDifcpaper.State==schema.CommercialPaper_TRADING {
cpaper.Owner=buyData.NewOwner
} else {
returnnil, fmt.Errorf(
"paper %s %s is not trading.current state = %s",
cpaper.Issuer, cpaper.PaperNumber, cpaper.State)
}
iferr=c.Event().Set(buyData); err!=nil {
returnnil, err
}
returncpaper, c.State().Put(cpaper)
}

Test chaincode functionality

And finally we should write tests to ensure our business logic is behaving as it should. Again, for brevity, we omitted most of the code from examples/cpaper_extended/chaincode_test.go. CCKit support chaincode testing with MockStub.

var_=Describe(`CommercialPaper`, func() {
paperChaincode:=testcc.NewMockStub(`commercial_paper`, NewCC())
BeforeSuite(func() {
// Init chaincode with admin identityexpectcc.ResponseOk(
paperChaincode.
From(testdata.GetTestIdentity(MspName, path.Join("testdata", "admin", "admin.pem"))).
Init())
})
Describe("Commercial Paper lifecycle", func() {
// ...It("Allow buyer to buy commercial paper", func() {
buyTransactionData:=&schema.BuyCommercialPaper{
Issuer: IssuerName,
PaperNumber: "0001",
CurrentOwner: IssuerName,
NewOwner: BuyerName,
Price: 95000,
PurchaseDate: ptypes.TimestampNow(),
}
expectcc.ResponseOk(paperChaincode.Invoke(`buy`, buyTransactionData))
queryResponse:=paperChaincode.Query("get", &schema.CommercialPaperId{
Issuer: IssuerName,
PaperNumber: "0001",
})
paper:=expectcc.PayloadIs(queryResponse, &schema.CommercialPaper{}).(*schema.CommercialPaper)
Expect(paper.Owner).To(Equal(BuyerName))
Expect(paper.State).To(Equal(schema.CommercialPaper_TRADING))
Expect(<-paperChaincode.ChaincodeEventsChannel).To(BeEquivalentTo(&peer.ChaincodeEvent{
EventName: `BuyCommercialPaper`,
Payload: testcc.MustProtoMarshal(buyTransactionData),
}))
paperChaincode.ClearEvents()
})
// ...
})
})