Module 5: Advanced Topics and Use Cases

5.4 Exercises and Assessment

This section provides hands-on exercises and assessment materials to help reinforce the advanced concepts covered in Module 5.

Exercise 1: Implementing External System Integration

In this exercise, you will create a simple integration between a Hyperledger Fabric network and an external system using REST APIs.

Prerequisites:

  • Completed Hyperledger Fabric test network setup from Module 4
  • Node.js and npm installed
  • Basic understanding of REST APIs

Step 1: Set Up the Project

Create a new directory for your integration project:

mkdir fabric-integration-exercise
cd fabric-integration-exercise
npm init -y

Install the required dependencies:

npm install express fabric-network cors body-parser

Step 2: Create the Express Server

Create a file named server.js with the following content:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { Gateway, Wallets } = require('fabric-network');
const fs = require('fs');
const path = require('path');

const app = express();
const port = 3000;

// Middleware
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Connect to the Fabric network
async function connectToNetwork() {
    try {
        // Load the network configuration
        const ccpPath = path.resolve(__dirname, 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new file system based wallet for managing identities
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);

        // Check to see if we've already enrolled the user
        const identity = await wallet.get('appUser');
        if (!identity) {
            console.log('An identity for the user "appUser" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return null;
        }

        // Create a new gateway for connecting to our peer node
        const gateway = new Gateway();
        await gateway.connect(ccp, { 
            wallet, 
            identity: 'appUser', 
            discovery: { enabled: true, asLocalhost: true } 
        });

        // Get the network (channel) our contract is deployed to
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network
        const contract = network.getContract('basic');

        return { gateway, contract };
    } catch (error) {
        console.error(`Failed to connect to the network: ${error}`);
        return null;
    }
}

// API endpoints
app.get('/api/assets', async (req, res) => {
    try {
        const { contract, gateway } = await connectToNetwork();
        if (!contract) {
            return res.status(500).json({ error: 'Failed to connect to the network' });
        }

        const result = await contract.evaluateTransaction('GetAllAssets');
        const assets = JSON.parse(result.toString());

        gateway.disconnect();
        res.json(assets);
    } catch (error) {
        console.error(`Failed to get assets: ${error}`);
        res.status(500).json({ error: error.message });
    }
});

app.get('/api/assets/:id', async (req, res) => {
    try {
        const { contract, gateway } = await connectToNetwork();
        if (!contract) {
            return res.status(500).json({ error: 'Failed to connect to the network' });
        }

        const result = await contract.evaluateTransaction('ReadAsset', req.params.id);
        const asset = JSON.parse(result.toString());

        gateway.disconnect();
        res.json(asset);
    } catch (error) {
        console.error(`Failed to get asset: ${error}`);
        res.status(500).json({ error: error.message });
    }
});

app.post('/api/assets', async (req, res) => {
    try {
        const { contract, gateway } = await connectToNetwork();
        if (!contract) {
            return res.status(500).json({ error: 'Failed to connect to the network' });
        }

        const { id, color, size, owner, appraisedValue } = req.body;

        await contract.submitTransaction(
            'CreateAsset',
            id,
            color,
            size.toString(),
            owner,
            appraisedValue.toString()
        );

        gateway.disconnect();
        res.status(201).json({ message: 'Asset created successfully' });
    } catch (error) {
        console.error(`Failed to create asset: ${error}`);
        res.status(500).json({ error: error.message });
    }
});

app.put('/api/assets/:id', async (req, res) => {
    try {
        const { contract, gateway } = await connectToNetwork();
        if (!contract) {
            return res.status(500).json({ error: 'Failed to connect to the network' });
        }

        const { color, size, owner, appraisedValue } = req.body;

        await contract.submitTransaction(
            'UpdateAsset',
            req.params.id,
            color,
            size.toString(),
            owner,
            appraisedValue.toString()
        );

        gateway.disconnect();
        res.json({ message: 'Asset updated successfully' });
    } catch (error) {
        console.error(`Failed to update asset: ${error}`);
        res.status(500).json({ error: error.message });
    }
});

app.delete('/api/assets/:id', async (req, res) => {
    try {
        const { contract, gateway } = await connectToNetwork();
        if (!contract) {
            return res.status(500).json({ error: 'Failed to connect to the network' });
        }

        await contract.submitTransaction('DeleteAsset', req.params.id);

        gateway.disconnect();
        res.json({ message: 'Asset deleted successfully' });
    } catch (error) {
        console.error(`Failed to delete asset: ${error}`);
        res.status(500).json({ error: error.message });
    }
});

// Start the server
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

Step 3: Create the Connection Profile

Copy the connection profile for Org1 from your Fabric network to your project directory as connection-org1.json.

Step 4: Create the User Enrollment Script

Create a file named enrollAdmin.js with the following content:

const { Wallets } = require('fabric-network');
const FabricCAServices = require('fabric-ca-client');
const fs = require('fs');
const path = require('path');

async function main() {
    try {
        // Load the network configuration
        const ccpPath = path.resolve(__dirname, 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new CA client for interacting with the CA
        const caInfo = ccp.certificateAuthorities['ca.org1.example.com'];
        const caTLSCACerts = caInfo.tlsCACerts.pem;
        const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName);

        // Create a new file system based wallet for managing identities
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the admin user
        const identity = await wallet.get('admin');
        if (identity) {
            console.log('An identity for the admin user "admin" already exists in the wallet');
            return;
        }

        // Enroll the admin user, and import the new identity into the wallet
        const enrollment = await ca.enroll({ enrollmentID: 'admin', enrollmentSecret: 'adminpw' });
        const x509Identity = {
            credentials: {
                certificate: enrollment.certificate,
                privateKey: enrollment.key.toBytes(),
            },
            mspId: 'Org1MSP',
            type: 'X.509',
        };
        await wallet.put('admin', x509Identity);
        console.log('Successfully enrolled admin user "admin" and imported it into the wallet');

    } catch (error) {
        console.error(`Failed to enroll admin user "admin": ${error}`);
        process.exit(1);
    }
}

main();

Create a file named registerUser.js with the following content:

const { Wallets } = require('fabric-network');
const FabricCAServices = require('fabric-ca-client');
const fs = require('fs');
const path = require('path');

async function main() {
    try {
        // Load the network configuration
        const ccpPath = path.resolve(__dirname, 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new CA client for interacting with the CA
        const caURL = ccp.certificateAuthorities['ca.org1.example.com'].url;
        const ca = new FabricCAServices(caURL);

        // Create a new file system based wallet for managing identities
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user
        const userIdentity = await wallet.get('appUser');
        if (userIdentity) {
            console.log('An identity for the user "appUser" already exists in the wallet');
            return;
        }

        // Check to see if we've already enrolled the admin user
        const adminIdentity = await wallet.get('admin');
        if (!adminIdentity) {
            console.log('An identity for the admin user "admin" does not exist in the wallet');
            console.log('Run the enrollAdmin.js application before retrying');
            return;
        }

        // Build a user object for authenticating with the CA
        const provider = wallet.getProviderRegistry().getProvider(adminIdentity.type);
        const adminUser = await provider.getUserContext(adminIdentity, 'admin');

        // Register the user, enroll the user, and import the new identity into the wallet
        const secret = await ca.register({
            affiliation: 'org1.department1',
            enrollmentID: 'appUser',
            role: 'client'
        }, adminUser);
        const enrollment = await ca.enroll({
            enrollmentID: 'appUser',
            enrollmentSecret: secret
        });
        const x509Identity = {
            credentials: {
                certificate: enrollment.certificate,
                privateKey: enrollment.key.toBytes(),
            },
            mspId: 'Org1MSP',
            type: 'X.509',
        };
        await wallet.put('appUser', x509Identity);
        console.log('Successfully registered and enrolled user "appUser" and imported it into the wallet');

    } catch (error) {
        console.error(`Failed to register user "appUser": ${error}`);
        process.exit(1);
    }
}

main();

Step 5: Enroll Admin and Register User

Run the following commands to enroll the admin and register a user:

node enrollAdmin.js
node registerUser.js

Step 6: Start the Server

Start the Express server:

node server.js

Step 7: Test the API

Use a tool like Postman or curl to test the API endpoints:

# Get all assets
curl http://localhost:3000/api/assets

# Create a new asset
curl -X POST http://localhost:3000/api/assets \
  -H "Content-Type: application/json" \
  -d '{"id":"asset1","color":"blue","size":5,"owner":"Tom","appraisedValue":300}'

# Get a specific asset
curl http://localhost:3000/api/assets/asset1

# Update an asset
curl -X PUT http://localhost:3000/api/assets/asset1 \
  -H "Content-Type: application/json" \
  -d '{"color":"red","size":10,"owner":"Tom","appraisedValue":400}'

# Delete an asset
curl -X DELETE http://localhost:3000/api/assets/asset1

Exercise Questions:

  1. How would you modify the API to implement authentication and authorization?
  2. What changes would be needed to support private data collections?
  3. How would you handle error scenarios, such as network disconnections or chaincode errors?
  4. How would you implement event listening to notify clients of changes to the ledger?
  5. What additional security measures would you implement for a production environment?

Exercise 2: Implementing Private Data Collections

In this exercise, you will implement a chaincode that uses private data collections to store sensitive information.

Prerequisites:

  • Completed Hyperledger Fabric test network setup from Module 4
  • Go programming language installed
  • Basic understanding of private data collections

Step 1: Create the Collection Definition JSON

Create a file named collections_config.json with the following content:

[
    {
        "name": "assetCollection",
        "policy": "OR('Org1MSP.member', 'Org2MSP.member')",
        "requiredPeerCount": 1,
        "maxPeerCount": 3,
        "blockToLive": 100000,
        "memberOnlyRead": true,
        "memberOnlyWrite": true
    },
    {
        "name": "assetPrivateDetails",
        "policy": "OR('Org1MSP.member')",
        "requiredPeerCount": 0,
        "maxPeerCount": 3,
        "blockToLive": 3,
        "memberOnlyRead": true,
        "memberOnlyWrite": true
    }
]

Step 2: Create the Chaincode

Create a directory for your chaincode:

mkdir -p private-asset-transfer/chaincode
cd private-asset-transfer/chaincode

Create a file named asset_transfer_private.go with the following content:

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// AssetTransfer provides functions for transferring assets
type AssetTransfer struct {
    contractapi.Contract
}

// Asset describes basic details of what makes up a simple asset
type Asset struct {
    ID             string `json:"id"`
    Color          string `json:"color"`
    Size           int    `json:"size"`
    Owner          string `json:"owner"`
}

// AssetPrivateDetails describes private details of an asset
type AssetPrivateDetails struct {
    ID             string `json:"id"`
    AppraisedValue int    `json:"appraisedValue"`
}

// CreateAsset creates a new asset with the given input
func (s *AssetTransfer) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
        return fmt.Errorf("failed to get asset: %v", err)
    }
    if exists {
        return fmt.Errorf("asset already exists: %s", id)
    }

    // Create asset object
    asset := Asset{
        ID:             id,
        Color:          color,
        Size:           size,
        Owner:          owner,
    }

    // Convert to JSON
    assetJSON, err := json.Marshal(asset)
    if err != nil {
        return fmt.Errorf("failed to marshal asset: %v", err)
    }

    // Save asset to public state
    err = ctx.GetStub().PutState(id, assetJSON)
    if err != nil {
        return fmt.Errorf("failed to put asset in public data: %v", err)
    }

    // Get private data from transient map
    transientMap, err := ctx.GetStub().GetTransient()
    if err != nil {
        return fmt.Errorf("error getting transient: %v", err)
    }

    // Asset private details must be passed in the transient field
    privateDetailsJSON, ok := transientMap["asset_private_details"]
    if !ok {
        return fmt.Errorf("asset_private_details key not found in the transient map")
    }

    var privateDetails AssetPrivateDetails
    err = json.Unmarshal(privateDetailsJSON, &privateDetails)
    if err != nil {
        return fmt.Errorf("failed to unmarshal private details: %v", err)
    }

    // Verify that the private details ID matches the public asset ID
    if privateDetails.ID != id {
        return fmt.Errorf("private details ID %s does not match asset ID %s", privateDetails.ID, id)
    }

    // Save private details to private data collection
    err = ctx.GetStub().PutPrivateData("assetPrivateDetails", id, privateDetailsJSON)
    if err != nil {
        return fmt.Errorf("failed to put private details: %v", err)
    }

    return nil
}

// 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) {
    privateDetailsJSON, err := ctx.GetStub().GetPrivateData("assetPrivateDetails", id)
    if err != nil {
        return nil, fmt.Errorf("failed to read private details: %v", err)
    }
    if privateDetailsJSON == nil {
        return nil, fmt.Errorf("the private details for %s do not exist", id)
    }

    var privateDetails AssetPrivateDetails
    err = json.Unmarshal(privateDetailsJSON, &privateDetails)
    if err != nil {
        return nil, err
    }

    return &privateDetails, nil
}

// UpdateAsset updates an existing asset with new values
func (s *AssetTransfer) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string) error {
    asset, err := s.ReadAsset(ctx, id)
    if err != nil {
        return err
    }

    // Update asset properties
    asset.Color = color
    asset.Size = size
    asset.Owner = owner

    assetJSON, err := json.Marshal(asset)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
}

// UpdateAssetPrivateDetails updates the private details of an asset
func (s *AssetTransfer) UpdateAssetPrivateDetails(ctx contractapi.TransactionContextInterface, id string) error {
    // Get private data from transient map
    transientMap, err := ctx.GetStub().GetTransient()
    if err != nil {
        return fmt.Errorf("error getting transient: %v", err)
    }

    // Asset private details must be passed in the transient field
    privateDetailsJSON, ok := transientMap["asset_private_details"]
    if !ok {
        return fmt.Errorf("asset_private_details key not found in the transient map")
    }

    var privateDetails AssetPrivateDetails
    err = json.Unmarshal(privateDetailsJSON, &privateDetails)
    if err != nil {
        return fmt.Errorf("failed to unmarshal private details: %v", err)
    }

    // Verify that the private details ID matches the asset ID
    if privateDetails.ID != id {
        return fmt.Errorf("private details ID %s does not match asset ID %s", privateDetails.ID, id)
    }

    // Save private details to private data collection
    err = ctx.GetStub().PutPrivateData("assetPrivateDetails", id, privateDetailsJSON)
    if err != nil {
        return fmt.Errorf("failed to put private details: %v", err)
    }

    return nil
}

// DeleteAsset deletes an asset from the world state
func (s *AssetTransfer) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
        return fmt.Errorf("failed to get asset: %v", err)
    }
    if !exists {
        return fmt.Errorf("asset does not exist: %s", id)
    }

    // Delete the asset from the state
    err = ctx.GetStub().DelState(id)
    if err != nil {
        return fmt.Errorf("failed to delete asset: %v", err)
    }

    // Delete private details of asset
    err = ctx.GetStub().DelPrivateData("assetPrivateDetails", id)
    if err != nil {
        return fmt.Errorf("failed to delete private details: %v", err)
    }

    return 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
}

// 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
}

func main() {
    assetChaincode, err := contractapi.NewChaincode(&AssetTransfer{})
    if err != nil {
        log.Panicf("Error creating asset-transfer-private chaincode: %v", err)
    }

    if err := assetChaincode.Start(); err != nil {
        log.Panicf("Error starting asset-transfer-private chaincode: %v", err)
    }
}

Step 3: Package and Install the Chaincode

Follow the steps from Module 4 to package and install the chaincode on your test network, making sure to include the collections configuration file.

Step 4: Create a Client Application

Create a file named app.js with the following content:

const { Gateway, Wallets } = require('fabric-network');
const fs = require('fs');
const path = require('path');

async function main() {
    try {
        // Load the network configuration
        const ccpPath = path.resolve(__dirname, 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new file system based wallet for managing identities
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user
        const identity = await wallet.get('appUser');
        if (!identity) {
            console.log('An identity for the user "appUser" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node
        const gateway = new Gateway();
        await gateway.connect(ccp, { 
            wallet, 
            identity: 'appUser', 
            discovery: { enabled: true, asLocalhost: true } 
        });

        // Get the network (channel) our contract is deployed to
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network
        const contract = network.getContract('private-asset-transfer');

        // Create a new asset with private data
        console.log('\n--> Submit Transaction: CreateAsset');

        const assetID = `asset${Math.floor(Math.random() * 10000)}`;
        const privateData = {
            id: assetID,
            appraisedValue: 5000
        };

        const transient = {
            asset_private_details: Buffer.from(JSON.stringify(privateData))
        };

        await contract.submitTransaction(
            'CreateAsset',
            assetID,
            'blue',
            '10',
            'Tom',
            transient
        );
        console.log('*** Result: committed');

        // Get public data
        console.log('\n--> Evaluate Transaction: ReadAsset');
        let result = await contract.evaluateTransaction('ReadAsset', assetID);
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Get private data (this will only work for Org1 members)
        console.log('\n--> Evaluate Transaction: ReadAssetPrivateDetails');
        result = await contract.evaluateTransaction('ReadAssetPrivateDetails', assetID);
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Update the asset
        console.log('\n--> Submit Transaction: UpdateAsset');
        await contract.submitTransaction('UpdateAsset', assetID, 'red', '20', 'Alice');
        console.log('*** Result: committed');

        // Update private data
        console.log('\n--> Submit Transaction: UpdateAssetPrivateDetails');
        const newPrivateData = {
            id: assetID,
            appraisedValue: 7000
        };

        const newTransient = {
            asset_private_details: Buffer.from(JSON.stringify(newPrivateData))
        };

        await contract.submitTransaction('UpdateAssetPrivateDetails', assetID, newTransient);
        console.log('*** Result: committed');

        // Get updated private data
        console.log('\n--> Evaluate Transaction: ReadAssetPrivateDetails');
        result = await contract.evaluateTransaction('ReadAssetPrivateDetails', assetID);
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Disconnect from the gateway
        gateway.disconnect();

    } catch (error) {
        console.error(`Failed to submit transaction: ${error}`);
        process.exit(1);
    }
}

function prettyJSONString(inputString) {
    return JSON.stringify(JSON.parse(inputString), null, 2);
}

main();

Step 5: Run the Application

Run the application to test the private data functionality:

node app.js

Exercise Questions:

  1. What happens if an organization that is not included in the private data collection policy tries to access the private data?
  2. How would you modify the chaincode to implement access control based on the client's organization?
  3. What are the implications of setting a low blockToLive value for private data?
  4. How would you implement a consent mechanism where the owner of an asset must approve access to its private data?
  5. What are the trade-offs between using private data collections and separate channels for privacy?

Exercise 3: Implementing a Supply Chain Solution

In this exercise, you will implement a simplified supply chain solution using Hyperledger Fabric.

Prerequisites:

  • Completed Hyperledger Fabric test network setup from Module 4
  • Go programming language installed
  • Basic understanding of supply chain concepts

Step 1: Design the Data Model

Create a file named data_model.md with the following content:

# Supply Chain Data Model

## Product
- ID (string): Unique identifier for the product
- Name (string): Name of the product
- Description (string): Description of the product
- SKU (string): Stock Keeping Unit
- Manufacturer (string): Name of the manufacturer
- ManufacturingDate (date): Date when the product was manufactured
- ExpiryDate (date): Date when the product expires (if applicable)
- BatchNumber (string): Batch or lot number
- Status (string): Current status of the product (e.g., MANUFACTURED, IN_TRANSIT, DELIVERED)

## Participant
- ID (string): Unique identifier for the participant
- Name (string): Name of the participant
- Type (string): Type of participant (e.g., MANUFACTURER, DISTRIBUTOR, RETAILER)
- Location (string): Location of the participant

## Shipment
- ID (string): Unique identifier for the shipment
- ProductIDs ([]string): List of product IDs in the shipment
- Sender (string): ID of the sender participant
- Receiver (string): ID of the receiver participant
- ShipmentDate (date): Date when the shipment was sent
- ExpectedDeliveryDate (date): Expected delivery date
- ActualDeliveryDate (date): Actual delivery date (if delivered)
- Status (string): Current status of the shipment (e.g., CREATED, IN_TRANSIT, DELIVERED)
- Conditions (map): Environmental conditions during shipment (e.g., temperature, humidity)

## Event
- Timestamp (date): Time when the event occurred
- Type (string): Type of event (e.g., MANUFACTURED, SHIPPED, RECEIVED)
- ParticipantID (string): ID of the participant who triggered the event
- ProductID (string): ID of the product related to the event (if applicable)
- ShipmentID (string): ID of the shipment related to the event (if applicable)
- Details (string): Additional details about the event

Step 2: Create the Chaincode

Create a directory for your chaincode:

mkdir -p supply-chain/chaincode
cd supply-chain/chaincode

Create a file named supply_chain.go with the following content:

package main

import (
    "encoding/json"
    "fmt"
    "time"

    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SupplyChain provides functions for managing a supply chain
type SupplyChain struct {
    contractapi.Contract
}

// Product represents a product in the supply chain
type Product struct {
    ID               string    `json:"id"`
    Name             string    `json:"name"`
    Description      string    `json:"description"`
    SKU              string    `json:"sku"`
    Manufacturer     string    `json:"manufacturer"`
    ManufacturingDate time.Time `json:"manufacturingDate"`
    ExpiryDate       time.Time `json:"expiryDate"`
    BatchNumber      string    `json:"batchNumber"`
    Status           string    `json:"status"`
    History          []Event   `json:"history"`
}

// Participant represents a participant in the supply chain
type Participant struct {
    ID       string `json:"id"`
    Name     string `json:"name"`
    Type     string `json:"type"`
    Location string `json:"location"`
}

// Shipment represents a shipment in the supply chain
type Shipment struct {
    ID                  string            `json:"id"`
    ProductIDs          []string          `json:"productIds"`
    Sender              string            `json:"sender"`
    Receiver            string            `json:"receiver"`
    ShipmentDate        time.Time         `json:"shipmentDate"`
    ExpectedDeliveryDate time.Time        `json:"expectedDeliveryDate"`
    ActualDeliveryDate  time.Time         `json:"actualDeliveryDate"`
    Status              string            `json:"status"`
    Conditions          map[string]string `json:"conditions"`
    History             []Event           `json:"history"`
}

// Event represents an event in the supply chain
type Event struct {
    Timestamp    time.Time `json:"timestamp"`
    Type         string    `json:"type"`
    ParticipantID string    `json:"participantId"`
    ProductID    string    `json:"productId"`
    ShipmentID   string    `json:"shipmentId"`
    Details      string    `json:"details"`
}

// RegisterParticipant registers a new participant in the supply chain
func (s *SupplyChain) RegisterParticipant(ctx contractapi.TransactionContextInterface, id string, name string, participantType string, location string) error {
    exists, err := s.ParticipantExists(ctx, id)
    if err != nil {
        return err
    }
    if exists {
        return fmt.Errorf("the participant %s already exists", id)
    }

    participant := Participant{
        ID:       id,
        Name:     name,
        Type:     participantType,
        Location: location,
    }

    participantJSON, err := json.Marshal(participant)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(fmt.Sprintf("PARTICIPANT_%s", id), participantJSON)
}

// CreateProduct creates a new product in the supply chain
func (s *SupplyChain) CreateProduct(ctx contractapi.TransactionContextInterface, id string, name string, description string, sku string, manufacturer string, manufacturingDate string, expiryDate string, batchNumber string) error {
    exists, err := s.ProductExists(ctx, id)
    if err != nil {
        return err
    }
    if exists {
        return fmt.Errorf("the product %s already exists", id)
    }

    // Check if manufacturer exists
    manufacturerExists, err := s.ParticipantExists(ctx, manufacturer)
    if err != nil {
        return err
    }
    if !manufacturerExists {
        return fmt.Errorf("the manufacturer %s does not exist", manufacturer)
    }

    // Parse dates
    mfgDate, err := time.Parse(time.RFC3339, manufacturingDate)
    if err != nil {
        return fmt.Errorf("invalid manufacturing date format: %v", err)
    }

    expDate, err := time.Parse(time.RFC3339, expiryDate)
    if err != nil {
        return fmt.Errorf("invalid expiry date format: %v", err)
    }

    // Get client identity
    clientID, err := s.getClientIdentity(ctx)
    if err != nil {
        return err
    }

    // Create the product
    product := Product{
        ID:               id,
        Name:             name,
        Description:      description,
        SKU:              sku,
        Manufacturer:     manufacturer,
        ManufacturingDate: mfgDate,
        ExpiryDate:       expDate,
        BatchNumber:      batchNumber,
        Status:           "MANUFACTURED",
        History: []Event{
            {
                Timestamp:    time.Now(),
                Type:         "MANUFACTURED",
                ParticipantID: manufacturer,
                ProductID:    id,
                Details:      fmt.Sprintf("Product %s was manufactured", id),
            },
        },
    }

    productJSON, err := json.Marshal(product)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(fmt.Sprintf("PRODUCT_%s", id), productJSON)
}

// CreateShipment creates a new shipment in the supply chain
func (s *SupplyChain) CreateShipment(ctx contractapi.TransactionContextInterface, id string, productIDs string, sender string, receiver string, shipmentDate string, expectedDeliveryDate string) error {
    exists, err := s.ShipmentExists(ctx, id)
    if err != nil {
        return err
    }
    if exists {
        return fmt.Errorf("the shipment %s already exists", id)
    }

    // Check if sender exists
    senderExists, err := s.ParticipantExists(ctx, sender)
    if err != nil {
        return err
    }
    if !senderExists {
        return fmt.Errorf("the sender %s does not exist", sender)
    }

    // Check if receiver exists
    receiverExists, err := s.ParticipantExists(ctx, receiver)
    if err != nil {
        return err
    }
    if !receiverExists {
        return fmt.Errorf("the receiver %s does not exist", receiver)
    }

    // Parse product IDs
    var products []string
    err = json.Unmarshal([]byte(productIDs), &products)
    if err != nil {
        return fmt.Errorf("invalid product IDs format: %v", err)
    }

    // Check if all products exist and update their status
    for _, productID := range products {
        product, err := s.ReadProduct(ctx, productID)
        if err != nil {
            return err
        }

        // Update product status
        product.Status = "IN_TRANSIT"

        // Add event to product history
        product.History = append(product.History, Event{
            Timestamp:    time.Now(),
            Type:         "SHIPPED",
            ParticipantID: sender,
            ProductID:    productID,
            ShipmentID:   id,
            Details:      fmt.Sprintf("Product %s was shipped in shipment %s", productID, id),
        })

        productJSON, err := json.Marshal(product)
        if err != nil {
            return err
        }

        err = ctx.GetStub().PutState(fmt.Sprintf("PRODUCT_%s", productID), productJSON)
        if err != nil {
            return err
        }
    }

    // Parse dates
    shipDate, err := time.Parse(time.RFC3339, shipmentDate)
    if err != nil {
        return fmt.Errorf("invalid shipment date format: %v", err)
    }

    expDeliveryDate, err := time.Parse(time.RFC3339, expectedDeliveryDate)
    if err != nil {
        return fmt.Errorf("invalid expected delivery date format: %v", err)
    }

    // Get client identity
    clientID, err := s.getClientIdentity(ctx)
    if err != nil {
        return err
    }

    // Create the shipment
    shipment := Shipment{
        ID:                  id,
        ProductIDs:          products,
        Sender:              sender,
        Receiver:            receiver,
        ShipmentDate:        shipDate,
        ExpectedDeliveryDate: expDeliveryDate,
        Status:              "IN_TRANSIT",
        Conditions:          make(map[string]string),
        History: []Event{
            {
                Timestamp:    time.Now(),
                Type:         "SHIPPED",
                ParticipantID: sender,
                ShipmentID:   id,
                Details:      fmt.Sprintf("Shipment %s was sent from %s to %s", id, sender, receiver),
            },
        },
    }

    shipmentJSON, err := json.Marshal(shipment)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(fmt.Sprintf("SHIPMENT_%s", id), shipmentJSON)
}

// UpdateShipmentConditions updates the environmental conditions of a shipment
func (s *SupplyChain) UpdateShipmentConditions(ctx contractapi.TransactionContextInterface, id string, conditions string) error {
    shipment, err := s.ReadShipment(ctx, id)
    if err != nil {
        return err
    }

    // Parse conditions
    var conditionsMap map[string]string
    err = json.Unmarshal([]byte(conditions), &conditionsMap)
    if err != nil {
        return fmt.Errorf("invalid conditions format: %v", err)
    }

    // Update shipment conditions
    for key, value := range conditionsMap {
        shipment.Conditions[key] = value
    }

    // Get client identity
    clientID, err := s.getClientIdentity(ctx)
    if err != nil {
        return err
    }

    // Add event to shipment history
    shipment.History = append(shipment.History, Event{
        Timestamp:    time.Now(),
        Type:         "CONDITIONS_UPDATED",
        ParticipantID: clientID,
        ShipmentID:   id,
        Details:      fmt.Sprintf("Shipment conditions updated: %s", conditions),
    })

    shipmentJSON, err := json.Marshal(shipment)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(fmt.Sprintf("SHIPMENT_%s", id), shipmentJSON)
}

// ReceiveShipment marks a shipment as received
func (s *SupplyChain) ReceiveShipment(ctx contractapi.TransactionContextInterface, id string, actualDeliveryDate string) error {
    shipment, err := s.ReadShipment(ctx, id)
    if err != nil {
        return err
    }

    // Check if shipment is in transit
    if shipment.Status != "IN_TRANSIT" {
        return fmt.Errorf("the shipment is not in transit")
    }

    // Parse date
    deliveryDate, err := time.Parse(time.RFC3339, actualDeliveryDate)
    if err != nil {
        return fmt.Errorf("invalid delivery date format: %v", err)
    }

    // Get client identity
    clientID, err := s.getClientIdentity(ctx)
    if err != nil {
        return err
    }

    // Update shipment
    shipment.Status = "DELIVERED"
    shipment.ActualDeliveryDate = deliveryDate

    // Add event to shipment history
    shipment.History = append(shipment.History, Event{
        Timestamp:    time.Now(),
        Type:         "RECEIVED",
        ParticipantID: shipment.Receiver,
        ShipmentID:   id,
        Details:      fmt.Sprintf("Shipment %s was received by %s", id, shipment.Receiver),
    })

    // Update all products in the shipment
    for _, productID := range shipment.ProductIDs {
        product, err := s.ReadProduct(ctx, productID)
        if err != nil {
            return err
        }

        // Update product status
        product.Status = "DELIVERED"

        // Add event to product history
        product.History = append(product.History, Event{
            Timestamp:    time.Now(),
            Type:         "RECEIVED",
            ParticipantID: shipment.Receiver,
            ProductID:    productID,
            ShipmentID:   id,
            Details:      fmt.Sprintf("Product %s was received by %s", productID, shipment.Receiver),
        })

        productJSON, err := json.Marshal(product)
        if err != nil {
            return err
        }

        err = ctx.GetStub().PutState(fmt.Sprintf("PRODUCT_%s", productID), productJSON)
        if err != nil {
            return err
        }
    }

    shipmentJSON, err := json.Marshal(shipment)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(fmt.Sprintf("SHIPMENT_%s", id), shipmentJSON)
}

// ReadParticipant returns the participant stored in the world state with given id
func (s *SupplyChain) ReadParticipant(ctx contractapi.TransactionContextInterface, id string) (*Participant, error) {
    participantJSON, err := ctx.GetStub().GetState(fmt.Sprintf("PARTICIPANT_%s", id))
    if err != nil {
        return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if participantJSON == nil {
        return nil, fmt.Errorf("the participant %s does not exist", id)
    }

    var participant Participant
    err = json.Unmarshal(participantJSON, &participant)
    if err != nil {
        return nil, err
    }

    return &participant, nil
}

// ReadProduct returns the product stored in the world state with given id
func (s *SupplyChain) ReadProduct(ctx contractapi.TransactionContextInterface, id string) (*Product, error) {
    productJSON, err := ctx.GetStub().GetState(fmt.Sprintf("PRODUCT_%s", id))
    if err != nil {
        return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if productJSON == nil {
        return nil, fmt.Errorf("the product %s does not exist", id)
    }

    var product Product
    err = json.Unmarshal(productJSON, &product)
    if err != nil {
        return nil, err
    }

    return &product, nil
}

// ReadShipment returns the shipment stored in the world state with given id
func (s *SupplyChain) ReadShipment(ctx contractapi.TransactionContextInterface, id string) (*Shipment, error) {
    shipmentJSON, err := ctx.GetStub().GetState(fmt.Sprintf("SHIPMENT_%s", id))
    if err != nil {
        return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if shipmentJSON == nil {
        return nil, fmt.Errorf("the shipment %s does not exist", id)
    }

    var shipment Shipment
    err = json.Unmarshal(shipmentJSON, &shipment)
    if err != nil {
        return nil, err
    }

    return &shipment, nil
}

// GetProductHistory returns the history of a product
func (s *SupplyChain) GetProductHistory(ctx contractapi.TransactionContextInterface, id string) ([]Event, error) {
    product, err := s.ReadProduct(ctx, id)
    if err != nil {
        return nil, err
    }

    return product.History, nil
}

// GetShipmentHistory returns the history of a shipment
func (s *SupplyChain) GetShipmentHistory(ctx contractapi.TransactionContextInterface, id string) ([]Event, error) {
    shipment, err := s.ReadShipment(ctx, id)
    if err != nil {
        return nil, err
    }

    return shipment.History, nil
}

// QueryProductsByManufacturer returns all products from a specific manufacturer
func (s *SupplyChain) QueryProductsByManufacturer(ctx contractapi.TransactionContextInterface, manufacturer string) ([]*Product, error) {
    queryString := fmt.Sprintf(`{"selector":{"manufacturer":"%s"}}`, manufacturer)

    return s.getQueryResultForQueryString(ctx, queryString, "PRODUCT_")
}

// QueryProductsByStatus returns all products with a specific status
func (s *SupplyChain) QueryProductsByStatus(ctx contractapi.TransactionContextInterface, status string) ([]*Product, error) {
    queryString := fmt.Sprintf(`{"selector":{"status":"%s"}}`, status)

    return s.getQueryResultForQueryString(ctx, queryString, "PRODUCT_")
}

// QueryShipmentsBySender returns all shipments from a specific sender
func (s *SupplyChain) QueryShipmentsBySender(ctx contractapi.TransactionContextInterface, sender string) ([]*Shipment, error) {
    queryString := fmt.Sprintf(`{"selector":{"sender":"%s"}}`, sender)

    return s.getQueryResultForShipments(ctx, queryString)
}

// QueryShipmentsByReceiver returns all shipments to a specific receiver
func (s *SupplyChain) QueryShipmentsByReceiver(ctx contractapi.TransactionContextInterface, receiver string) ([]*Shipment, error) {
    queryString := fmt.Sprintf(`{"selector":{"receiver":"%s"}}`, receiver)

    return s.getQueryResultForShipments(ctx, queryString)
}

// ParticipantExists returns true when participant with given ID exists in world state
func (s *SupplyChain) ParticipantExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
    participantJSON, err := ctx.GetStub().GetState(fmt.Sprintf("PARTICIPANT_%s", id))
    if err != nil {
        return false, fmt.Errorf("failed to read from world state: %v", err)
    }

    return participantJSON != nil, nil
}

// ProductExists returns true when product with given ID exists in world state
func (s *SupplyChain) ProductExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
    productJSON, err := ctx.GetStub().GetState(fmt.Sprintf("PRODUCT_%s", id))
    if err != nil {
        return false, fmt.Errorf("failed to read from world state: %v", err)
    }

    return productJSON != nil, nil
}

// ShipmentExists returns true when shipment with given ID exists in world state
func (s *SupplyChain) ShipmentExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
    shipmentJSON, err := ctx.GetStub().GetState(fmt.Sprintf("SHIPMENT_%s", id))
    if err != nil {
        return false, fmt.Errorf("failed to read from world state: %v", err)
    }

    return shipmentJSON != nil, nil
}

// Helper function to get client identity
func (s *SupplyChain) getClientIdentity(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 for rich queries for products
func (s *SupplyChain) getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string, prefix string) ([]*Product, error) {
    resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
    if err != nil {
        return nil, err
    }
    defer resultsIterator.Close()

    var products []*Product
    for resultsIterator.HasNext() {
        queryResponse, err := resultsIterator.Next()
        if err != nil {
            return nil, err
        }

        // Skip if the key doesn't have the correct prefix
        if prefix != "" && !strings.HasPrefix(queryResponse.Key, prefix) {
            continue
        }

        var product Product
        err = json.Unmarshal(queryResponse.Value, &product)
        if err != nil {
            return nil, err
        }
        products = append(products, &product)
    }

    return products, nil
}

// Helper function for rich queries for shipments
func (s *SupplyChain) getQueryResultForShipments(ctx contractapi.TransactionContextInterface, queryString string) ([]*Shipment, error) {
    resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
    if err != nil {
        return nil, err
    }
    defer resultsIterator.Close()

    var shipments []*Shipment
    for resultsIterator.HasNext() {
        queryResponse, err := resultsIterator.Next()
        if err != nil {
            return nil, err
        }

        // Skip if the key doesn't have the correct prefix
        if !strings.HasPrefix(queryResponse.Key, "SHIPMENT_") {
            continue
        }

        var shipment Shipment
        err = json.Unmarshal(queryResponse.Value, &shipment)
        if err != nil {
            return nil, err
        }
        shipments = append(shipments, &shipment)
    }

    return shipments, nil
}

func main() {
    chaincode, err := contractapi.NewChaincode(&SupplyChain{})
    if err != nil {
        fmt.Printf("Error creating supply chain chaincode: %v\n", err)
        return
    }

    if err := chaincode.Start(); err != nil {
        fmt.Printf("Error starting supply chain chaincode: %v\n", err)
    }
}

Step 3: Create a Client Application

Create a file named app.js with the following content:

const { Gateway, Wallets } = require('fabric-network');
const fs = require('fs');
const path = require('path');

async function main() {
    try {
        // Load the network configuration
        const ccpPath = path.resolve(__dirname, 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new file system based wallet for managing identities
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user
        const identity = await wallet.get('appUser');
        if (!identity) {
            console.log('An identity for the user "appUser" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node
        const gateway = new Gateway();
        await gateway.connect(ccp, { 
            wallet, 
            identity: 'appUser', 
            discovery: { enabled: true, asLocalhost: true } 
        });

        // Get the network (channel) our contract is deployed to
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network
        const contract = network.getContract('supply-chain');

        // Register participants
        console.log('\n--> Submit Transaction: RegisterParticipant - Manufacturer');
        await contract.submitTransaction(
            'RegisterParticipant',
            'manufacturer1',
            'ABC Manufacturing',
            'MANUFACTURER',
            'New York, USA'
        );
        console.log('*** Result: committed');

        console.log('\n--> Submit Transaction: RegisterParticipant - Distributor');
        await contract.submitTransaction(
            'RegisterParticipant',
            'distributor1',
            'XYZ Distribution',
            'DISTRIBUTOR',
            'Chicago, USA'
        );
        console.log('*** Result: committed');

        console.log('\n--> Submit Transaction: RegisterParticipant - Retailer');
        await contract.submitTransaction(
            'RegisterParticipant',
            'retailer1',
            '123 Retail',
            'RETAILER',
            'Los Angeles, USA'
        );
        console.log('*** Result: committed');

        // Create products
        console.log('\n--> Submit Transaction: CreateProduct');
        await contract.submitTransaction(
            'CreateProduct',
            'product1',
            'Laptop',
            'High-performance laptop',
            'SKU123456',
            'manufacturer1',
            '2023-01-15T00:00:00Z',
            '2028-01-15T00:00:00Z',
            'BATCH001'
        );
        console.log('*** Result: committed');

        console.log('\n--> Submit Transaction: CreateProduct');
        await contract.submitTransaction(
            'CreateProduct',
            'product2',
            'Smartphone',
            'Latest smartphone model',
            'SKU789012',
            'manufacturer1',
            '2023-01-20T00:00:00Z',
            '2028-01-20T00:00:00Z',
            'BATCH002'
        );
        console.log('*** Result: committed');

        // Create shipment from manufacturer to distributor
        console.log('\n--> Submit Transaction: CreateShipment');
        await contract.submitTransaction(
            'CreateShipment',
            'shipment1',
            JSON.stringify(['product1', 'product2']),
            'manufacturer1',
            'distributor1',
            '2023-02-01T00:00:00Z',
            '2023-02-05T00:00:00Z'
        );
        console.log('*** Result: committed');

        // Update shipment conditions
        console.log('\n--> Submit Transaction: UpdateShipmentConditions');
        await contract.submitTransaction(
            'UpdateShipmentConditions',
            'shipment1',
            JSON.stringify({
                'temperature': '22C',
                'humidity': '45%',
                'location': 'Pittsburgh, USA'
            })
        );
        console.log('*** Result: committed');

        // Receive shipment at distributor
        console.log('\n--> Submit Transaction: ReceiveShipment');
        await contract.submitTransaction(
            'ReceiveShipment',
            'shipment1',
            '2023-02-04T00:00:00Z'
        );
        console.log('*** Result: committed');

        // Create shipment from distributor to retailer
        console.log('\n--> Submit Transaction: CreateShipment');
        await contract.submitTransaction(
            'CreateShipment',
            'shipment2',
            JSON.stringify(['product1']),
            'distributor1',
            'retailer1',
            '2023-02-10T00:00:00Z',
            '2023-02-12T00:00:00Z'
        );
        console.log('*** Result: committed');

        // Update shipment conditions
        console.log('\n--> Submit Transaction: UpdateShipmentConditions');
        await contract.submitTransaction(
            'UpdateShipmentConditions',
            'shipment2',
            JSON.stringify({
                'temperature': '23C',
                'humidity': '40%',
                'location': 'Columbus, USA'
            })
        );
        console.log('*** Result: committed');

        // Receive shipment at retailer
        console.log('\n--> Submit Transaction: ReceiveShipment');
        await contract.submitTransaction(
            'ReceiveShipment',
            'shipment2',
            '2023-02-11T00:00:00Z'
        );
        console.log('*** Result: committed');

        // Query product history
        console.log('\n--> Evaluate Transaction: GetProductHistory');
        let result = await contract.evaluateTransaction('GetProductHistory', 'product1');
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Query shipment history
        console.log('\n--> Evaluate Transaction: GetShipmentHistory');
        result = await contract.evaluateTransaction('GetShipmentHistory', 'shipment1');
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Query products by manufacturer
        console.log('\n--> Evaluate Transaction: QueryProductsByManufacturer');
        result = await contract.evaluateTransaction('QueryProductsByManufacturer', 'manufacturer1');
        console.log(`*** Result: ${prettyJSONString(result.toString())}`);

        // Disconnect from the gateway
        gateway.disconnect();

    } catch (error) {
        console.error(`Failed to submit transaction: ${error}`);
        process.exit(1);
    }
}

function prettyJSONString(inputString) {
    return JSON.stringify(JSON.parse(inputString), null, 2);
}

main();

Exercise Questions:

  1. How would you extend the supply chain solution to include quality control checks at each stage?
  2. What additional data would you need to track for regulatory compliance in industries like pharmaceuticals or food?
  3. How would you implement a recall process for defective products?
  4. What privacy considerations would you need to address in a multi-organization supply chain network?
  5. How would you integrate IoT devices to automatically update shipment conditions?

Assessment

Multiple Choice Questions:

  1. Which of the following is NOT a valid integration pattern for Hyperledger Fabric? a) API-based integration b) Event-based integration c) Off-chain data storage d) Direct database integration

  2. When implementing private data collections, which of the following is true? a) All organizations on the channel can access the private data b) Private data is stored in a separate database outside the blockchain c) Private data is stored in the blockchain but is only accessible to authorized organizations d) Private data can only be accessed by the organization that created it

  3. Which security feature in Hyperledger Fabric allows you to prove a statement is true without revealing additional information? a) Private data collections b) Zero-knowledge proofs c) Hardware Security Modules d) Channel policies

  4. In a supply chain solution built on Hyperledger Fabric, which of the following would be the most appropriate way to handle sensitive pricing information? a) Store it in the public ledger b) Store it in a private data collection c) Store it off-chain with a hash reference on-chain d) Both b and c are appropriate solutions

  5. When integrating IoT devices with Hyperledger Fabric, which of the following is a best practice? a) Have IoT devices submit transactions directly to the blockchain b) Use an IoT gateway to aggregate and validate data before submitting to the blockchain c) Store all IoT data on the blockchain for maximum transparency d) Give IoT devices full admin access to the blockchain network

Short Answer Questions:

  1. Explain the trade-offs between using private data collections and separate channels for privacy in Hyperledger Fabric.

  2. Describe how you would implement a consent mechanism for sharing sensitive data in a healthcare blockchain network.

  3. Explain how zero-knowledge proofs can be used to enhance privacy in a Hyperledger Fabric application.

  4. Describe the key considerations when integrating Hyperledger Fabric with existing enterprise systems.

  5. Explain how you would design a Hyperledger Fabric network for a global supply chain with multiple competing organizations.

Project:

Design and implement a simplified version of one of the following Hyperledger Fabric applications:

  1. A supply chain tracking system for a specific industry (e.g., pharmaceuticals, food, luxury goods)
  2. A trade finance platform for international trade
  3. A healthcare data sharing network with privacy controls
  4. A land registry system with multi-organization approval processes

Your implementation should include: - Network architecture diagram - Organization and channel design - Chaincode implementation (at least one smart contract) - Client application for interacting with the network - Documentation explaining the design decisions and privacy considerations

Submit your project as a GitHub repository with clear instructions for setting up and running the application.