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.