Hyperledger Fabric development -- developing user chain code

preface

The code of this series of articles is in Go language.

Chain code file chaincode Basic structure of go

First, let's look at a sample code. All chain codes contain this basic structure

package main

import (
	"fmt"
	"github.com/hyperledger/fabric/core/chaincode/shim"
	pb "github.com/hyperledger/fabric/protos/peer"
)

type SimpleChaincode struct {
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Success(nil)
}

func main() {
	err := shim.Start(new(SimpleChaincode))
	if err != nil {
		fmt.Printf("Error starting Simple chaincode: %s", err)
	}
}

The following is the case of chaincode in fabric samples sacc.go Let's learn about the various parts of chaincode and how to write it.

  1. Import resource package and definition structure

First, import fmt, shim, and protos packages.

The Chaincode interface defines Init and Invoke functions. The shim package defines common methods such as Success and Error. The ChaincodeStubInterface interface of the shim package provides a set of methods through which you can easily operate ledger data.

For details, please refer to the official documents:

  • https://pkg.go.dev/github.com/hyperledger/fabric/core/chaincode/shim?tab=doc#ChaincodeStubInterface
  • https://pkg.go.dev/github.com/hyperledger/fabric/protos/peer?tab=doc

Next, define a structure SimpleAsset with empty attribute as the receiving parameter of the chain code method.

package main

import (
	"fmt"

	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/protos/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}
  1. Init function

The init function is used to initialize the chain code. Init method will be called when chain code is instantiated and upgraded.

When the chain code is initialized, chaincodestubinterface is called Getstringargs function to get the parameters entered during initialization. In this example, we expect to pass in two parameters as a key/value pair. Next, use key/value as chaincodestubinterface Parameter of putstate. If shim returns the correct message to the client, it indicates that the initialization is successful.

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
	// Get the args from the transaction proposal
	args := stub.GetStringArgs()
	if len(args) != 2 {
		return shim.Error("Incorrect arguments. Expecting a key and a value")
	}

	// Set up any variables or assets here by calling stub.PutState()

	// We store the key and the value on the ledger
	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
		return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
	}
	return shim.Success(nil)
}
  1. Invoke function

The invoke method is called when the client interacts with the chain code. In this example, there are only set and get methods. The set method is used to assign values to assets, and the get method is used to query asset balances.

First call C haincodestubinterface Getfunctionandparameters obtains the function name and parameters, then verifies the function name according to set or get, calls the corresponding method, and passes shim Success or shim Error returns the corresponding result.

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
	// Extract the function and args from the transaction proposal
	fn, args := stub.GetFunctionAndParameters()

	var result string
	var err error
	if fn == "set" {
		result, err = set(stub, args)
	} else { // assume 'get' even if fn is nil
		result, err = get(stub, args)
	}
	if err != nil {
		return shim.Error(err.Error())
	}

	// Return the result as success payload
	return shim.Success([]byte(result))
}
  1. Implementation method

In the Inovke method, set and get functions are called. The following is the specific implementation.

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 2 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
	}

	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
		return "", fmt.Errorf("Failed to set asset: %s", args[0])
	}
	return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 1 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key")
	}

	value, err := stub.GetState(args[0])
	if err != nil {
		return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
	}
	if value == nil {
		return "", fmt.Errorf("Asset not found: %s", args[0])
	}
	return string(value), nil
}
  1. Add main function

The main function is used to call the Start function to Start the SimpleAsset chain code. It should be noted that the main function is called only when the chain code is instantiated.

// main function starts up the chaincode in the container during instantiate
func main() {
	if err := shim.Start(new(SimpleAsset)); err != nil {
		fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
	}
}
  1. Compiler

The last step is to compile chaincode. Enter go build after the terminal enters the path where the chaincode file is located

If there is no problem with compilation, you can test the chain code.

Chain code method programming routine

First, check the in fabric samples provided by Hyperledger Fabric sacc set method of case

func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    // Check whether the number of parameters meets the requirements
	if len(args) != 2 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
	}
    // Write to ledger
	err := stub.PutState(args[0], []byte(args[1]))
    // Judge whether err is empty. If it is not empty, it means that there is an error in writing to the ledger
	if err != nil {
		return "", fmt.Errorf("Failed to set asset: %s", args[0])
	}
    // Generally, the return in the function is peer Data of response structure
    // In this chain code, the reason is that the Invoke function receives and processes method calls differently
	return args[1], nil
}

Next, let's take a look at the relatively complete chain code case "marble asset management" in fabric samples marbles_chaincode.go marble structure and initMarble method in (New Chinese Notes)

// Customize the necessary structure
type marble struct {
	ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
	Name       string `json:"name"`    //the fieldtags are needed to keep case from bouncing around
	Color      string `json:"color"`
	Size       int    `json:"size"`
	Owner      string `json:"owner"`
	// The variable name is capitalized, otherwise the variable cannot be read when serialized as a json string
}

// ============================================================
// initMarble - create a new marble, store into chaincode state
// ============================================================
func (t *SimpleChaincode) initMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	var err error
	// Input parameters for official example:
	//   0       1       2     3
	// "asdf", "blue", "35", "bob"
	// Number of inspection parameters
	if len(args) != 4 {
		return shim.Error("Incorrect number of arguments. Expecting 4")
	}

	fmt.Println("- start init marble")
	// Verify the correctness of the parameter (judge whether the parameter is empty)
	// As required, none of the four parameters can be empty
	if len(args[0]) <= 0 {
		return shim.Error("1st argument must be a non-empty string")
	}
	if len(args[1]) <= 0 {
		return shim.Error("2nd argument must be a non-empty string")
	}
	if len(args[2]) <= 0 {
		return shim.Error("3rd argument must be a non-empty string")
	}
	if len(args[3]) <= 0 {
		return shim.Error("4th argument must be a non-empty string")
	}
	marbleName := args[0]
	color := strings.ToLower(args[1])
	owner := strings.ToLower(args[3])
	// Convert the third parameter from string to int
	// This step is still to verify the correctness of the parameters. If err is not empty, the conversion data is wrong, indicating that the input parameters do not meet the requirements
	size, err := strconv.Atoi(args[2])
	if err != nil {
		return shim.Error("3rd argument must be a numeric string")
	}

	// Verify that the data exists
	// Get the value corresponding to marbleName in the ledger
	marbleAsBytes, err := stub.GetState(marbleName)
	if err != nil {
		return shim.Error("Failed to get marble: " + err.Error())
	} else if marbleAsBytes != nil {  // If the returned value is not empty, the data already exists
        // Sometimes it is to judge that the data already exists, and sometimes it is to judge that the data does not exist, depending on the situation
        // This is a new marble, so it should be judged that the data does not exist. If it does exist, an error should be reported
        // Without this step, the data newly written into the ledger will directly overwrite the original data, which will lead to the loss of the original data
		fmt.Println("This marble already exists: " + marbleName)
		return shim.Error("This marble already exists: " + marbleName)
	}

	// ==== Create marble object and marshal to JSON ====
	objectType := "marble"
	// Instantiate a marble object
	marble := &marble{objectType, marbleName, color, size, owner}
	// Serialize the object as a json string. marbleJSONasBytes is of type [] byte
	// Because when using PutState, the value must be of type [] byte
	marbleJSONasBytes, err := json.Marshal(marble)
	if err != nil {
		return shim.Error(err.Error())
	}
	//Alternatively, build the marble json string manually if you don't want to use struct marshalling
	//marbleJSONasString := `{"docType":"Marble",  "name": "` + marbleName + `", "color": "` + color + `", "size": ` + strconv.Itoa(size) + `, "owner": "` + owner + `"}`
	//marbleJSONasBytes := []byte(str)

	// === Save marble to state ===
	// Write to ledger
	err = stub.PutState(marbleName, marbleJSONasBytes)
	if err != nil {
		return shim.Error(err.Error())
	}

	//  ==== Index the marble to enable color-based range queries, e.g. return all blue marbles ====
	//  An 'index' is a normal key/value entry in state.
	//  The key is a composite key, with the elements that you want to range query on listed first.
	//  In our case, the composite key is based on indexName~color~name.
	//  This will enable very efficient state range queries based on composite keys matching indexName~color~*
	indexName := "color~name"
	// Create composite key
	colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marble.Color, marble.Name})
	if err != nil {
		return shim.Error(err.Error())
	}
	//  Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
	//  Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
	value := []byte{0x00}
	// Write to ledger
	stub.PutState(colorNameIndexKey, value)

	// ==== Marble saved and indexed. Return success ====
	fmt.Println("- end init marble")
    // return is peer Data of response structure
    // shim.Success() and shim Error() is Pb Data of response structure
	return shim.Success(nil)
}

According to the above and the smart contracts of other open source projects, the overall basic routine of chain code function is:

Even when the business logic is complex, other operation logic is added on this basis.

Chain code writing taboo

Chain codes are executed in the Docker container of multiple nodes in isolation, that is, for the same transaction, the transaction will be executed many times in the whole blockchain network, and the execution times depend on the choice of endorsement strategy. For example, you can select all nodes in the chain to execute, or select a node in an organization to execute.

The client will compare the transaction simulation results returned from different nodes. If they are different, the transaction will be rejected and regarded as invalid. It will not be sent to the sorting node for sorting, which means that the transaction has failed. Therefore, the following contents should be avoided in the chain code:

  • Random function: the result of each random function is different, so it does not meet the requirements.
  • System time: Although the time for simulating transactions is very fast, there will be small differences when executed in multiple containers, resulting in different transaction simulation results.
  • Unstable external dependence: ① accessing external resources may expose system vulnerabilities and introduce security threats to your chain code; ② Access to external dependencies leads to different results. For example, the chain code function needs to visit a website and obtain some data. If the content or status (downtime) of the website changes before and after, the execution results of transaction simulation will be different.

thank

  • https://learnblockchain.cn/books/enterprise/chapter5_04%20chaincode_dev.html#%E9%93%BE%E7%A0%81%E5%BC%80%E5%8F%91
  • https://gist.github.com/arnabkaycee/d4c10a7f5c01f349632b42b67cee46db
  • https://my.oschina.net/u/3843525/blog/3167899

ps. my blog address www.bluewhale52.com

Keywords: Go Blockchain

Added by stridox on Sun, 23 Jan 2022 00:24:09 +0200