The Basic contract is a sample hyperledger blockchain contract that is provided by IBM to help you to get started with blockchain development and integration on the IBM Watson IoT Platform. You can use the Basic contract sample to create a blockchain contract that tracks and stores asset data from a device that is connected to your Watson IoT Platform organization.
The following information is provided to help you to get started with the Basic sample:
- Overview
- Requirements
- Developing your IBM Blockchain hyperledger by using the Basic contract sample
- Invoking the contract
- Generating code
- Next steps
The Basic blockchain sample, simple_contract_hyperledger.go, is the default smart contract for getting started with writing blockchain contracts. The Basic contract includes create, read, update, and delete asset data operations for tracking device data on the IBM Blockchain ledger. Like all other IBM Blockchain contracts, the Basic contract sample is developed in the Go programming language. IBM plan to support contracts that are written in other languages soon.
You can run your smart contracts for Blockchain from either the command line or from a REST interface. For more information, see Developing smart contracts for Watson IoT Platform blockchain integration.
As outlined in Basic sample, IBM Blockchain contract code includes the following methods:
Method | Description |
---|---|
deploy |
Used to deploy a smart contract |
invoke |
Used to update a smart contract |
query |
Used to query a smart contract |
Note: In the Basic contract sample, the deploy
method is called init
.
When you call any of the methods, you must first pass a JSON string that includes the function name and a set of arguments as key-value pairs to the chain code instance. You can define multiple methods within the same contract.
To create a simple contract to create, read, update, and delete asset data, use the following methods:
Method | Provides |
---|---|
'readAssetSchemas' | The methods and associated properties of the JSON schema contract |
'readAssetSamples' | An example of the sample JSON data |
For more information about setting up REST and Swagger, see here.
The Watson IoT Platform includes a data mapping component to route your data to the Blockchain contract. To correctly map the Watson IoT Platform event properties to the corresponding Blockchain contract properties, use the following required functions:
Function | Used by the data mapping function to.... |
---|---|
updateAsset |
|
readAssetSchemas |
Expose the function names and properties that are required by the contract so that the data mapper can correctly map the event properties to the contract properties |
The Basic contract is an example recipe that is designed for you to customize and use to experiment with contract development for Blockchain. The current version of the Basic contract is simplistic with similar functions to create and update asset data.
To use the Basic sample simple_contract_hyperledger.go sample contract as a foundation to develop your own use cases into deployable chaincode, complete the following procedure:
- Download the Basic contract sample.
- Create the base contract and implement version control.
- Define the asset data structure. The Basic contact sample defines an asset that is in transit and is being tracked as it moves from one location to another. The asset data includes a unique alphanumeric ID that identifies the asset, and other information, for example, location, temperature, and carrier. For more information, see
- Initialize the contract.
- Define the invoke methods.
- Define the query methods for how the contract data is read.
- Define the callbacks.
- Develop the contract further
Detailed information about how to complete each step is hyperlinked to the sections that follow.
Download the Basic blockchain contract sample from the IBM Blockchain contracts repository on GitHub. You need the following Basic sample files, which are provided in the repository folder:
- simple_contract_hyperledger.go|The Basic contract source file
- samples.go|?|
- schemas.go|?|
Note: When you install the IBM Blockchain sample environment on Bluemix, the Basic simple_contract_hyperledger.go contract is deployed by default.
To create the base source file for your Blockchain contract:
- Create a copy of simple_contract_hyperledger.go, which is the main source file of the Basic contract.
- Using an editor of your choice, open simple_contract_hyperledger.go.
- Define the SimpleChaincode struct, as outlined in the following code snippet:
type SimpleChaincode struct { }
- Initialize the contract with a version number, as outlined in the following code snippet:
const MYVERSION string = "1.0"
- Define a contract state to keep track of the contract version.
type ContractState struct {
Version string `json:"version"`
}
var contractState = ContractState{MYVERSION}
Note: You can eventually increase the complexity of the ContractState
code in your contract, for example, you can add more contract state details, introduce asset state alerts, and other items that are outlined in other more advanced examples.
The Basic contract provides the Blockchain contract code that is required for an asset that is in transit and is also being tracked as it moves from one location to another. In this example, the following asset data is tracked:
- Unique alphanumeric ID that identifies the asset
- Location
- Temperature
- Carrier
The following code provides an example of how to define the data structure for the example scenario of an asset in transit:
type Geolocation struct {
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
}
type AssetState struct {
AssetID *string `json:"assetID,omitempty"` // all assets must have an ID, primary key of contract
Location *Geolocation `json:"location,omitempty"` // current asset location
Temperature *float64 `json:"temperature,omitempty"` // asset temp
Carrier *string `json:"carrier,omitempty"` // the name of the carrier
}
Location has two attributes, latitude and longitude.
All the fields in the data structure definition are defined as pointers. A pointer is indicated by a preceding asterix character ('*'), for example, '*'float64.
When a JSON string is marshaled to a struct
, if the string does not have a particular field, a default value for the data type is assigned. Because a blank value is valid for a string, and a zero (0) character is also a valid number, incorrect data values might be assigned to fields with missing data types. To work around this problem, use pointers. If the pointer field of a struct isn't represented in the input JSON string, that field's value is not mapped. Another way to work around the problem is to feed the data into a map. Unlike a 'struct', a map doesn't have a specific data structure and accepts the submitted data without requiring and assigning default field values. However, when a specific data structure is expected and types must be validated, it is good to use structs, for example, in the case of a specific IoT device in this example.
Another advantage of marshaling JSON values to or from a struct is that Golang makes every effort to match the fields and look for perfect case-insensitive matches, ignoring the order of the fields. Whereas when you define map elements, you need to handle case mismatches and assert data types explicitly.
The init
function is one of the three required functions of the chaincode and initializes the contract. The other required functions of the chaincode are invoke
and query
. The init
function is called as a 'deploy' function to deploy the chaincode to the fabric. Notice the signature of the function. The init
, invoke
, and query
functions share the following arguments:
Argument | Description |
---|---|
Shim | ? |
Function name | |
Array of strings |
Other functions that are called from the standard functions typically contain the following arguments:
Argument | Description |
---|---|
Pointer to the shim | Enables the function to interact with the shim |
Function name | |
Arg attributes | Arguments that are to be consumed by those functions |
// ************************************
// deploy callback mode
// ************************************
func (t *SimpleChaincode) Init(stub *shim.ChaincodeStub, function string, args []string) ([]byte, error) {
var stateArg ContractState
var err error
The following example shows how you can create an instance of ContractState, which was declared earlier in step 2.
if len(args) != 1 {
return nil, errors.New("init expects one argument, a JSON string with tagged version string")
}
err = json.Unmarshal([]byte(args[0]), &stateArg)
if err != nil {
return nil, errors.New("Version argument unmarshal failed: " + fmt.Sprint(err))
}
Note: In this example, the contract version information is held for an instance of the contract. Only one argument is exposed, which is a JSON encoded string that submits the version value.
If the version that is submitted is different from the version in the contract, an error scenario occurs, which is a validation feature. The business logic of a contract can evolve and change over time, and multiple versions of the same contract might be expected to co-exist on the same chain, managing different assets. The version check validation feature ensures that the correct version of the contract is run.
The following example checks to see whether the version submitted matches the version that is defined in the contract. If the version does not match the version that is defined in the contract an error condition is raised and the contract code stops running.
if stateArg.Version != MYVERSION {
return nil, errors.New("Contract version " + MYVERSION + " must match version argument: " + stateArg.Version)
}
If the versions match, the contract code continues, and the following code to write the contract state to the ledger is run:
contractStateJSON, err := json.Marshal(stateArg)
if err != nil {
return nil, errors.New("Marshal failed for contract state" + fmt.Sprint(err))
}
err = stub.PutState(CONTRACTSTATEKEY, contractStateJSON)
if err != nil {
return nil, errors.New("Contract state failed PUT to ledger: " + fmt.Sprint(err))
}
return nil, nil
}
Define the invoke
methods for the create, read, update, and delete operations in your contract, which is where most of the contract action occurs. The createAsset
, updateAsset
and deleteAsset
methods of the create, retrieve, update, and delete interface are explained in this section.
You can bundle the create and update implementation together in a common function called createOrupdateAsset
. The createAsset
and updateAsset
functions make a call to the createOrupdateAsset
function as outlined in the following example:
/******************** `createAsset` ********************/
func (t *SimpleChaincode) `createAsset`(stub *shim.ChaincodeStub, args []string) ([]byte, error) {
_,erval:=t. createOr`updateAsset`(stub, args)
return nil, erval
}
//******************** `updateAsset` ********************/
func (t *SimpleChaincode) `updateAsset`(stub *shim.ChaincodeStub, args []string) ([]byte, error) {
_,erval:=t. createOr`updateAsset`(stub, args)
return nil, erval
}
If the asset doesn't exist, it is classified as a new asset and is created. If the asset does exists, the new record is classified as an update and the asset is updated with the new values that are retrieved. For example, if, in the new record, only temperature and location values are submitted, only those values of the asset get updated in the shim. If carrier data exists, it is maintained in the state that it was previously received.
The first step of the
createOrupdateAssetcommon function is a call to
validateInput, which is explained later in
‘Validate input data`.
stateIn, err = t.`validateInput`(args)
if err != nil {
return nil, err
}
After you run the code, to confirm whether the asset exists, insert a call to the shim.GetState
method. If the asset does not exist, it is a create scenario and the data is saved as received. If the asset exists, a call is made to the mergePartialState
method to merge old and new data and the updated record gets written to the shim.
//Partial updates introduced here
// Check if asset record existed in stub
assetBytes, err:= stub.GetState(assetID)
if err != nil || len(assetBytes)==0{
// This implies that this is a 'create' scenario
stateStub = stateIn // The record that goes into the stub is the one that came in
} else {
// This is an update scenario
err = json.Unmarshal(assetBytes, &stateStub)
if err != nil {
err = errors.New("Unable to unmarshal JSON data from stub")
return nil, err
// state is an empty instance of asset state
}
// Merge partial state updates
stateStub, err =t.mergePartialState(stateStub,stateIn)
if err != nil {
err = errors.New("Unable to merge state")
return nil,err
}
}
Use a query
method to define how the contract data is read. The Basic contract sample uses the following blockchain query implementation methods:
- Read asset data (readAsset) method
- Read asset object model (readAssetObjectModel) method
ThereadAsset
method returns the asset data that is stored in the ledger for the asset ID that is input. Method signature is similar to the other methods as is the call to validateInput
. The only difference is that in this method, the asset ID is used to fetch the asset data from the state and return it, and an error is raised if the asset data does not exist. Note that the same stub GetState call that was used previously in this sample is also used here.
assetID = *stateIn.AssetID
// Get the state from the ledger
assetBytes, err:= stub.GetState(assetID)
if err != nil || len(assetBytes) ==0{
err = errors.New("Unable to get asset state from ledger")
return nil, err
}
err = json.Unmarshal(assetBytes, &state)
if err != nil {
err = errors.New("Unable to unmarshal state data that is obtained from ledger")
return nil, err
}
The readAssetObjectModel
method returns the object model of the asset data set that the contract expects. The output of the readAssetObjectModel
method helps the user interface and mapping component understand the structure of the input JSON string that is expected by the contract. The signature is similar to the others because this is a query
method.
In the following example, the readAssetObjectModel
method creates an empty instance of the AssetState
definition, marshals it to a JSON string, and then returns it so that the caller knows the input data structure.
func (t *SimpleChaincode) readAssetObjectModel(stub *shim.ChaincodeStub, args []string) ([]byte, error) {
var state `AssetState` = AssetState{}
// Marshal and return
stateJSON, err := json.Marshal(state)
if err != nil {
return nil, err
}
return stateJSON, nil
}
The read asset sample returns a sample of the schema to the consumer of the contract by reading the contents of the samples.go file that is in the project folder.
The read asset schema sample returns the schema for the contract in JSON format to the consumer of the contract by reading the contents of the schemas.go file.
The schemas.go and the samples.go files are provided with the Basic contract sample. The trade_lane_contract_hyperledger advanced contract sample demonstrates how you can generate schema files by using the go generate
command.
In IBM Blockchain contracts, the init
method handles init
commands, which are the ones that are called as part of the 'deploy' phase when the code is run. The invoke
method handles invoke
method calls and the query
method handles invocations of type 'query'. In the previous step, you implemented the 'init' comand, so you are now ready to write some simple invoke and query methods to call the following functions:
createAsset
updateAsset
deleteAsset
readAsset
readAssetObjectModel
readAssetSchemas
readAssetSamples
The following example is for the invoke
implementation.
func (t *SimpleChaincode) Invoke(stub *shim.ChaincodeStub, function string, args []string) ([]byte, error) {
// Handle different functions
if function == "`createAsset`" {
// create assetID
return t.`createAsset`(stub, args)
} else if function == "`updateAsset`" {
// create assetID
return t.`updateAsset`(stub, args)
} else if function == "deleteAsset" {
// Deletes an asset by ID from the ledger
return t.deleteAsset(stub, args)
}
return nil, errors.New("Received unknown invocation: " + function)
}
The following example is for the query
implementation:
func (t *SimpleChaincode) Query(stub *shim.ChaincodeStub, function string, args []string) ([]byte, error) {
// Handle different functions
if function == "readAsset" {
// gets the state for an assetID as a JSON struct
return t.readAsset(stub, args)
} else if function =="readAssetObjectModel" {
return t.readAssetObjectModel(stub, args)
} else if function == "readAssetSamples" {
// returns selected sample objects
return t.readAssetSamples(stub, args)
} else if function == "readAssetSchemas" {
// returns selected sample objects
return t.readAssetSchemas(stub, args)
}
return nil, errors.New("Received unknown invocation: " + function)
}
The validateInput
function is not an implementation of an IBM Blockchain deploy, invoke, or query methods, but is a common function that is called by all of the invoke
type functions (createAsset
, updateAsset
and deleteAsset
) and thereadAsset
'query' function that does the following:
- Validates the input data for the right number of arguments,
- Unmarshals the input data to the asset state and
- Checks for the asset id.
func (t *SimpleChaincode) `validateInput`(args []string) (stateIn AssetState, err error) {
var assetID string // asset ID
var state AssetState = AssetState{} // The calling function is expecting an object of type AssetState
if len(args) !=1 {
err = errors.New("Incorrect number of arguments. Expecting a JSON strings with mandatory assetID")
return state, err
}
jsonData:=args[0]
assetID = ""
stateJSON := []byte(jsonData)
err = json.Unmarshal(stateJSON, &stateIn)
if err != nil {
err = errors.New("Unable to unmarshal input JSON data")
return state, err
// state is an empty instance of asset state
}
// was assetID present?
// The nil check is required because the asset id is a pointer.
// If no value comes in from the json input string, the values are set to nil
if stateIn.AssetID !=nil {
assetID = strings.TrimSpace(*stateIn.AssetID)
if assetID==""{
err = errors.New("AssetID not passed")
return state, err
}
} else {
err = errors.New("Asset ID is mandatory in the input JSON data")
return state, err
}
stateIn.AssetID = &assetID
return stateIn, nil
}
The Basic contract supports partial updates of data. The mergePartialState
function iteratively scans old and new records, replacing the old values with the new values when it identifies a field with updates. The merged record is then used by the updatepdateAsset
function.
The following snippet provides an example of how the mergePartialState
function is defined:
func (t *SimpleChaincode) mergePartialState(oldState AssetState, newState AssetState) (AssetState, error) {
old := reflect.ValueOf(&oldState).Elem()
new := reflect.ValueOf(&newState).Elem()
for i := 0; i < old.NumField(); i++ {
oldOne:=old.Field(i)
newOne:=new.Field(i)
if ! reflect.ValueOf(newOne.Interface()).IsNil() {
oldOne.Set(reflect.Value(newOne))
}
}
return oldState, nil
}
The 'main' function creates a 'shim' instance.
Note that we have a fmt.Printf command here. Printf and Println commands can easily be used to debug the chaincode as we write / modify and test chaincode. However, since the chaincode runs inside a docker container, these will get written to the log of the docker instance and space gets filled up very quickly, especially if the environment is running in debug mode. It is prudent to limit or avoid Print statements in actual deployed chaincode as the chaincode is essentially supposed to run in the background.
To avoid space issues, implement a procedure for docker log management, for example, you can implement Logrotate, which is supported since Docker V1.8. You can also optionally incorporate contract logging implementations in your contracts.
func main() {
err := shim.Start(new(SimpleChaincode))
if err != nil {
fmt.Printf("Error starting Simple Chaincode: %s", err)
}f
}
Use the deleteAsset
method to remove asset data from the current blockchain state by asset ID. Blockchain is a decentralized transaction ledger. Every single validated transaction is retained in the blockchain. When the record for a specific asset is created and updated in the blockchain, all activity is recorded in the ledger. The deleteAsset
method removes all data for the specified asset from the current state but does not remove it from the blockchain. If you need to retrieve or view any data pertaining to an asset that was deleted from the current state, you can still retrieve it from the blockchain ledger if you need to.
The following code snippet provides an example of how you can delete assets from the current state:
........
........
assetID = *stateIn.AssetID
// Delete the key / asset from the ledger
err = stub.DelState(assetID)
if err != nil {
err = errors.New("DELSTATE failed! : "+ fmt.Sprint(err))
return nil, err
}
.......
.......
Important: IBM Blockchain retains every single validated transaction in a decentralized peer-to-peer fashion. Peers can keep a copy of the complete ledger, meaning that every piece of data can be potentially retrieved and inspected at any point in time. IBM Blockchain maintains a state database in the context of a contract, which is somewhat analogous to Ethereum's state database or Bitcoin's UTXO set. Use the asset delete feature to remove your asset data from the current state as and when you need to. An advantage of the peer-to-peer feature is that in contracts where history is maintained, typically you query the state database rather than finding all transactions for a given contract and simulating the state history yourself.
When you complete all of the required steps in Developing your IBM Blockchain hyperledger by using the Basic contract sample you will have a simple contract to manage Watson IoT Platform asset data on the blockchain. Your contract will use a simple create, retrieve, update, and delete operations interface. The next step is to use the contract by invoking it. IBM Blockchain contracts expose REST APIs and these can be invoked through the command-line, or you can make REST API calls by using either Swagger or Node.js. No matter which method you use, the actual syntax of the calls that are made to a chaincode does not change. Later on in the more advanced contacts, you can see an example of a sample UI that provides a more user-friendly way of calling the contract.
As mentioned earlier, IBM Blockchain understands deploy
, invoke
, and query
calls, of which must contain a JSON string as input. The input string must also include a Function key and an Args key. The Function key is the name of the actual function you are calling, and the Args are the arguments you want to pass on to it.
There are several IBM Blockchain resources in GitHub that explain in more detail how you can set up the DevNet and Sandbox environments, and also how to test contracts in the command line. The Sandbox environment is particularly useful for writing new contracts or modifying existing ones as it provides you with an environment to test and debug chaincode without needing to set up a complete IBM Bluemix Hyperledger network. Let's see some examples of the command-line and rest calls to our sample chaincode.
When the network is running, the contract is built, and your instance is registered, you can start calling the contract that you created. If you are using Swagger or another REST interface, the HTTP server and the Swagger or Node.js must be set up as outlined here.
Note: In the Sandbox, you can specify the contract instance name. In a real peer network, when you register the contract, it will have a lengthy alphanumeric name that is returned by the network and that is the name that you must use. Alternately you can use the GitHub path to point to the contract.
The following code provides an example call to deploy
your contract:
./peer chaincode deploy -n ex01 -c '{"Function":"init", "Args": ["{\"version\":\"1.0\"}"]}'
Where:
- Contract name in the Sandbox is ex01
- Peer executable is called with the chaincode deploy command
- Name of the contract is specified as 'ex01'
- Arguments are in JSON format
- Function name is 'init'
- Arguments are listed inside Args: []
Note: Make a note of the signature. Also, note that in IBM Blockchain the content of Args[] can be any string(s), however, since our contract is about Watson IoT Platform data, which is in JSON format, the sample contract requires a JSON string inside Args[].
In command line Sandbox mode, in the tab where you egistered the contract, you can see the response of the Shim, stating that 'init' was received and was successful. If your contract includes print statements, which are useful for debugging, they will also be displayed. Remove print statements when the contract is deployed. IBM Blockchain deploy
and invoke
calls are asynchronous.
A REST call using curl
would look like the following code example:
curl -X POST --header 'Content-Type: application/json' --header 'Accept:
application/json' -d '{
"type": "GOLANG",
"chaincodeID": {
"name": "ex01"
},
"ctorMsg": {
"function": "init",
"args": [
"{\"version\":\"1.0\"}"
]
}
}' 'http://127.0.0.1:3000/devops/deploy'
The following snippet provides an example of a Swagger call that uses the deploy section of the IBM Blockchain REST API:
{
"type": "GOLANG",
"chaincodeID": {
"name": "ex01"
},
"ctorMsg": {
"function": "init",
"args": [
"{\"version\":\"1.0\"}"
]
},
"secureContext": "string",
"confidentialityLevel": "PUBLIC"
}
The syntax of the function call does not change based on how you call the contract. You need to specify the chaincode method, the function, and the arguments.
You can also explore how to call the other functions that were previously implemented.
Let's see how our asset's Schema looks like, so that we know what functions to call, and what parameters to pass for the function:
./peer chaincode query -l golang -n ex01 -c '{"Function":"readAssetSchemas", "Args":[]}'
The following call returns a lengthy JSON object that lists all functions and their properties, with descriptions. The object also indicates clearly which properties are mandatory. The schema can be consumed by applications for easy interaction with the contract. In fact the monitoring_ui example does just this.
###readAssetSamples
The readAssetSamples function provides a sample of what might be passed in.
./peer chaincode query -l golang -n ex01 -c '{"Function":"readAssetSamples", "Args":[]}'
When you establish what to pass in, you can explore and call the other methods.
./peer chaincode invoke -l golang -n ex01 -c '{"Function":"`createAsset`", "Args":["{\"assetID\":\"CON123\", \"location\":{\"latitude\":34, \"longitude\":23}, \"temperature\":89, \"carrier\":\"ABCCARRIER\"}"]}'
./peer chaincode query -l golang -n n ex01 -c '{"Function":"readAsset", "Args":["{\"assetID\":\"CON123\"}"]}'
Which returns:
{"assetID":"CON123","location":{"latitude":34,"longitude":23},"temperature":89,"carrier":"ABCCARRIER"}
./peer chaincode invoke -l golang -n ex01 -c '{"Function":"`updateAsset`", "Args":["{\"assetID\":\"CON123\", \"location\":{\"latitude\":56, \"longitude\":23}, \"temperature\":78, \"carrier\":\"PQRCARRIER\"}"]}'
anotherreadAsset
with return the updated asset data [??]
./peer chaincode invoke -l golang -n ex01 -c '{"Function":"deleteAsset", "Args":["{\"assetID\":\"CON123\"}"]}
Depending on the content of your contract, you might need to regenerate the deployable contract schema and sample from the GoLang file. for example, if you include a '//'' go generate directive in your code. To generate contract code, use the following go generate
command:
//go:generate go run scripts/generate_go_schema.go
After you create your contract, deploy it to your blockchain (IBM Blockchain or Hyperledger). Then use the Watson IoT Platform to route your device data to your contract. For more information, see Developing for blockchain
There are more sample contracts available for download in the IBM Blockchain samples folder on GitHub. Experiment with the available samples, for example, the Trade Lane blockchain contract. Use the samples to enhance your Blockchain smart contracts further.
IBM Blockchain also provides a sandbox environment for you to test contract code before deploying it to your peer network. For more information about how to set up the sandbox environment, see Writing, building, and running chaincode in a development environment.