Module 3: Practical Chaincode Development
3.4 Practical Chaincode Development
This section provides hands-on examples and practical guidance for developing chaincode in Hyperledger Fabric. We'll walk through the complete lifecycle of a chaincode project, from initial design to testing and deployment.
Asset Transfer Example
Let's develop a comprehensive asset transfer chaincode that demonstrates key concepts and best practices. This example will include asset creation, transfer, querying, and access control.
Use Case Definition
Our asset transfer chaincode will manage the lifecycle of assets with the following requirements:
- Assets have properties including ID, type, owner, and value
- Users can create new assets
- Owners can transfer assets to other users
- Users can query assets by ID, owner, or type
- Only authorized users can perform specific operations
- Asset history must be tracked
- Private data should be used for sensitive information
Data Model Design
First, let's define our data model:
// Asset represents a general asset that is managed by the chaincode
type Asset struct {
ID string `json:"id"`
Type string `json:"type"`
Owner string `json:"owner"`
PublicValue int `json:"publicValue"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
Status string `json:"status"`
}
// AssetPrivateDetails contains private details about an asset
type AssetPrivateDetails struct {
ID string `json:"id"`
AppraisedValue int `json:"appraisedValue"`
Secret string `json:"secret"`
}
Chaincode Implementation in Go
Let's implement the chaincode in Go:
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// AssetTransfer provides functions for managing assets
type AssetTransfer struct {
contractapi.Contract
}
// Asset represents a general asset that is managed by the chaincode
type Asset struct {
ID string `json:"id"`
Type string `json:"type"`
Owner string `json:"owner"`
PublicValue int `json:"publicValue"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
Status string `json:"status"`
}
// AssetPrivateDetails contains private details about an asset
type AssetPrivateDetails struct {
ID string `json:"id"`
AppraisedValue int `json:"appraisedValue"`
Secret string `json:"secret"`
}
// InitLedger adds a base set of assets to the ledger
func (s *AssetTransfer) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{
ID: "asset1",
Type: "electronics",
Owner: "tom",
PublicValue: 100,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
Status: "active",
},
{
ID: "asset2",
Type: "vehicle",
Owner: "jane",
PublicValue: 500,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
Status: "active",
},
}
for _, asset := range assets {
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state: %v", err)
}
}
return nil
}
// CreateAsset issues a new asset to the world state with given details
func (s *AssetTransfer) CreateAsset(ctx contractapi.TransactionContextInterface, id string, assetType string, owner string, publicValue int) error {
// Check if asset already exists
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
// Check if caller has permission to create assets
err = s.verifyClientOrgMatchesPeerOrg(ctx)
if err != nil {
return fmt.Errorf("CreateAsset unauthorized: %v", err)
}
// Create asset object and marshal to JSON
asset := Asset{
ID: id,
Type: assetType,
Owner: owner,
PublicValue: publicValue,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
Status: "active",
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// Put asset in world state
return ctx.GetStub().PutState(id, assetJSON)
}
// CreateAssetWithPrivateData issues a new asset to the world state with given details and private data
func (s *AssetTransfer) CreateAssetWithPrivateData(ctx contractapi.TransactionContextInterface, id string, assetType string, owner string, publicValue int) error {
// First create the public asset
err := s.CreateAsset(ctx, id, assetType, owner, publicValue)
if err != nil {
return err
}
// Get the private data from the transient map
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
}
// Private data must be passed in the transient field
privateDataJSON, ok := transientMap["asset_private_details"]
if !ok {
return fmt.Errorf("asset_private_details key not found in the transient map")
}
var privateData AssetPrivateDetails
err = json.Unmarshal(privateDataJSON, &privateData)
if err != nil {
return fmt.Errorf("failed to unmarshal private details: %v", err)
}
// Verify that the private data ID matches the public data ID
if privateData.ID != id {
return fmt.Errorf("private data ID %s does not match public data ID %s", privateData.ID, id)
}
// Put private data in the private data collection
return ctx.GetStub().PutPrivateData("assetPrivateCollection", id, privateDataJSON)
}
// ReadAsset returns the asset stored in the world state with given id
func (s *AssetTransfer) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// ReadAssetPrivateDetails returns the private details of an asset
func (s *AssetTransfer) ReadAssetPrivateDetails(ctx contractapi.TransactionContextInterface, id string) (*AssetPrivateDetails, error) {
privateDataJSON, err := ctx.GetStub().GetPrivateData("assetPrivateCollection", id)
if err != nil {
return nil, fmt.Errorf("failed to read private data: %v", err)
}
if privateDataJSON == nil {
return nil, fmt.Errorf("the private details for %s do not exist", id)
}
var privateData AssetPrivateDetails
err = json.Unmarshal(privateDataJSON, &privateData)
if err != nil {
return nil, err
}
return &privateData, nil
}
// UpdateAsset updates an existing asset in the world state
func (s *AssetTransfer) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, assetType string, owner string, publicValue int) error {
// Check if asset exists
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
// Verify that the caller is the owner
clientID, err := s.getSubmittingClientIdentity(ctx)
if err != nil {
return err
}
if clientID != asset.Owner {
return fmt.Errorf("submitting client %s is not the owner of the asset", clientID)
}
// Update asset
asset.Type = assetType
asset.Owner = owner
asset.PublicValue = publicValue
asset.UpdatedAt = time.Now().Unix()
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// TransferAsset updates the owner field of an asset in the world state
func (s *AssetTransfer) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
// Check if asset exists
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
// Verify that the caller is the owner
clientID, err := s.getSubmittingClientIdentity(ctx)
if err != nil {
return err
}
if clientID != asset.Owner {
return fmt.Errorf("submitting client %s is not the owner of the asset", clientID)
}
// Update owner and timestamp
asset.Owner = newOwner
asset.UpdatedAt = time.Now().Unix()
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// Update state
return ctx.GetStub().PutState(id, assetJSON)
}
// DeleteAsset deletes an asset from the world state
func (s *AssetTransfer) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
// Check if asset exists
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
// Verify that the caller is the owner
clientID, err := s.getSubmittingClientIdentity(ctx)
if err != nil {
return err
}
if clientID != asset.Owner {
return fmt.Errorf("submitting client %s is not the owner of the asset", clientID)
}
// Delete asset
return ctx.GetStub().DelState(id)
}
// GetAllAssets returns all assets found in world state
func (s *AssetTransfer) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// Range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
// GetAssetsByOwner returns all assets owned by a specific owner
func (s *AssetTransfer) GetAssetsByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]*Asset, error) {
queryString := fmt.Sprintf(`{"selector":{"owner":"%s"}}`, owner)
return s.getAssetsByQuery(ctx, queryString)
}
// GetAssetsByType returns all assets of a specific type
func (s *AssetTransfer) GetAssetsByType(ctx contractapi.TransactionContextInterface, assetType string) ([]*Asset, error) {
queryString := fmt.Sprintf(`{"selector":{"type":"%s"}}`, assetType)
return s.getAssetsByQuery(ctx, queryString)
}
// GetAssetHistory returns the history of an asset
func (s *AssetTransfer) GetAssetHistory(ctx contractapi.TransactionContextInterface, id string) ([]Asset, error) {
resultsIterator, err := ctx.GetStub().GetHistoryForKey(id)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var history []Asset
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
if len(response.Value) > 0 {
err = json.Unmarshal(response.Value, &asset)
if err != nil {
return nil, err
}
} else {
// Asset was deleted
asset = Asset{
ID: id,
Status: "deleted",
}
}
history = append(history, asset)
}
return history, nil
}
// AssetExists returns true when asset with given ID exists in world state
func (s *AssetTransfer) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
// Helper function to get assets by query
func (s *AssetTransfer) getAssetsByQuery(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) {
resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
// Helper function to get the submitting client identity
func (s *AssetTransfer) getSubmittingClientIdentity(ctx contractapi.TransactionContextInterface) (string, error) {
clientID, err := ctx.GetClientIdentity().GetID()
if err != nil {
return "", fmt.Errorf("failed to get client identity: %v", err)
}
return clientID, nil
}
// Helper function to verify that client org matches peer org
func (s *AssetTransfer) verifyClientOrgMatchesPeerOrg(ctx contractapi.TransactionContextInterface) error {
clientMSPID, err := ctx.GetClientIdentity().GetMSPID()
if err != nil {
return fmt.Errorf("failed getting client's MSPID: %v", err)
}
peerMSPID, err := shim.GetMSPID()
if err != nil {
return fmt.Errorf("failed getting peer's MSPID: %v", err)
}
if clientMSPID != peerMSPID {
return fmt.Errorf("client from org %s is not authorized to create assets with peer from org %s", clientMSPID, peerMSPID)
}
return nil
}
func main() {
assetTransferChaincode, err := contractapi.NewChaincode(&AssetTransfer{})
if err != nil {
fmt.Printf("Error creating asset-transfer chaincode: %v\n", err)
return
}
if err := assetTransferChaincode.Start(); err != nil {
fmt.Printf("Error starting asset-transfer chaincode: %v\n", err)
}
}
Private Data Collection Configuration
For the private data collection used in our chaincode, we need to define a configuration file:
[
{
"name": "assetPrivateCollection",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 1,
"maxPeerCount": 3,
"blockToLive": 100000,
"memberOnlyRead": true,
"memberOnlyWrite": true,
"endorsementPolicy": {
"signaturePolicy": "OR('Org1MSP.member', 'Org2MSP.member')"
}
}
]
Chaincode Deployment Process
Let's walk through the process of deploying our asset transfer chaincode:
1. Package the Chaincode
First, we need to package the chaincode:
# Create a directory for the chaincode
mkdir -p asset-transfer-chaincode
cd asset-transfer-chaincode
# Create the chaincode files
# - main.go (the code we wrote above)
# - go.mod (dependencies)
# - collections_config.json (private data configuration)
# Create go.mod file
cat > go.mod << EOF
module asset-transfer
go 1.16
require (
github.com/hyperledger/fabric-chaincode-go v0.0.0-20210718160520-38d29fabecb9
github.com/hyperledger/fabric-contract-api-go v1.1.1
)
EOF
# Package the chaincode
cd ..
peer lifecycle chaincode package asset-transfer.tar.gz --path ./asset-transfer-chaincode --lang golang --label asset-transfer_1.0
2. Install the Chaincode on Peers
Next, we install the chaincode on the peers of each organization:
# Set environment variables for Org1
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
# Install the chaincode on Org1 peer
peer lifecycle chaincode install asset-transfer.tar.gz
# Set environment variables for Org2
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
# Install the chaincode on Org2 peer
peer lifecycle chaincode install asset-transfer.tar.gz
3. Approve the Chaincode Definition
Each organization needs to approve the chaincode definition:
# Get the package ID
peer lifecycle chaincode queryinstalled
# Set the package ID (replace with the actual ID from the previous command)
export CC_PACKAGE_ID=asset-transfer_1.0:hash
# Approve for Org1
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name asset-transfer --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --collections-config ./asset-transfer-chaincode/collections_config.json
# Approve for Org2
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name asset-transfer --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --collections-config ./asset-transfer-chaincode/collections_config.json
4. Commit the Chaincode Definition
Finally, we commit the chaincode definition to the channel:
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name asset-transfer --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --collections-config ./asset-transfer-chaincode/collections_config.json
Interacting with the Chaincode
Now that our chaincode is deployed, let's interact with it:
Initialize the Ledger
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"InitLedger","Args":[]}'
Create a New Asset
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAsset","Args":["asset3", "jewelry", "alice", "1000"]}'
Create an Asset with Private Data
export ASSET_PRIVATE_DETAILS=$(echo -n '{"id":"asset4","appraisedValue":2000,"secret":"confidential information"}' | base64 | tr -d \\n)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAssetWithPrivateData","Args":["asset4", "art", "bob", "1500"]}' --transient "{\"asset_private_details\":\"$ASSET_PRIVATE_DETAILS\"}"
Query an Asset
peer chaincode query -C mychannel -n asset-transfer -c '{"function":"ReadAsset","Args":["asset1"]}'
Query Private Data
peer chaincode query -C mychannel -n asset-transfer -c '{"function":"ReadAssetPrivateDetails","Args":["asset4"]}'
Transfer an Asset
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset1", "charlie"]}'
Get Asset History
peer chaincode query -C mychannel -n asset-transfer -c '{"function":"GetAssetHistory","Args":["asset1"]}'
Testing Strategies
Effective testing is crucial for chaincode development. Here are some strategies:
Unit Testing
Create unit tests for each chaincode function:
// TestCreateAsset tests the CreateAsset function
func TestCreateAsset(t *testing.T) {
// Create a MockTransactionContext and MockChaincodeStub
mockStub := new(MockChaincodeStub)
transactionContext := MockTransactionContext{
mockStub: mockStub,
}
// Create a SmartContract
contract := AssetTransfer{}
// Set up mock behavior
mockStub.On("GetState", "asset1").Return([]byte{}, nil)
mockStub.On("PutState", "asset1", mock.Anything).Return(nil)
// Call the function being tested
err := contract.CreateAsset(&transactionContext, "asset1", "electronics", "tom", 100)
// Assert expectations
require.NoError(t, err)
mockStub.AssertCalled(t, "GetState", "asset1")
mockStub.AssertCalled(t, "PutState", "asset1", mock.Anything)
}
Integration Testing
Test the chaincode in a real network environment:
# Start the test network
./network.sh up createChannel -c mychannel -ca
# Deploy the chaincode
./network.sh deployCC -ccn asset-transfer -ccp ../asset-transfer-chaincode -ccl go -ccep "AND('Org1MSP.peer','Org2MSP.peer')" -cccg ../asset-transfer-chaincode/collections_config.json
# Run test script
./test-asset-transfer.sh
Performance Testing
Test chaincode performance under load:
# Create a load testing script
cat > load-test.sh << EOF
#!/bin/bash
# Set environment variables
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=\${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=\${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
# Run 100 transactions
for i in {1..100}
do
asset_id="loadtest_asset_\$i"
echo "Creating asset \$asset_id"
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile \${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer --peerAddresses localhost:7051 --tlsRootCertFiles \${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles \${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAsset","Args":["\$asset_id", "test", "loadtester", "1"]}' &
# Limit concurrent requests
if (( \$i % 10 == 0 )); then
wait
fi
done
wait
echo "Load test completed"
EOF
chmod +x load-test.sh
./load-test.sh
Debugging Techniques
Debugging chaincode can be challenging. Here are some techniques:
Logging
Add logging statements to your chaincode:
import (
"fmt"
"log"
)
// Inside a chaincode function
log.Printf("Processing asset: %s", id)
Chaincode Logs
View chaincode logs in the Docker container:
# Find the chaincode container
docker ps | grep asset-transfer
# View logs
docker logs -f <container_id>
Peer Logs
Examine peer logs for chaincode-related issues:
# View peer logs
docker logs -f peer0.org1.example.com
Transaction Validation
Check transaction validation status:
# Get the transaction ID from invoke output
export TX_ID=<transaction_id>
# Query transaction by ID
peer channel fetch transaction $TX_ID.block -c mychannel
configtxlator proto_decode --input $TX_ID.block --type common.Block | jq .
Best Practices for Production Chaincode
When preparing chaincode for production, follow these best practices:
Security
- Validate all inputs to prevent injection attacks
- Implement proper access control checks
- Use private data collections for sensitive information
- Follow the principle of least privilege
- Regularly audit chaincode for security vulnerabilities
Performance
- Minimize ledger reads and writes
- Use composite keys and indexes efficiently
- Implement pagination for large result sets
- Optimize query patterns
- Benchmark and profile chaincode under realistic loads
Maintainability
- Follow a consistent coding style
- Document all functions and parameters
- Use meaningful variable and function names
- Break complex functions into smaller, focused ones
- Write comprehensive tests
Upgradability
- Design with future upgrades in mind
- Maintain backward compatibility when possible
- Document state changes between versions
- Have a clear upgrade process
- Test upgrades thoroughly before deployment
Monitoring and Operations
- Implement proper logging
- Add metrics for monitoring
- Create operational documentation
- Have rollback procedures
- Establish a governance process for changes
By following these practical guidelines and examples, you'll be well-equipped to develop, test, and deploy effective chaincode solutions on Hyperledger Fabric. The asset transfer example demonstrates many of the key concepts and best practices covered throughout this module.