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:
- How would you modify the API to implement authentication and authorization?
- What changes would be needed to support private data collections?
- How would you handle error scenarios, such as network disconnections or chaincode errors?
- How would you implement event listening to notify clients of changes to the ledger?
- 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:
- What happens if an organization that is not included in the private data collection policy tries to access the private data?
- How would you modify the chaincode to implement access control based on the client's organization?
- What are the implications of setting a low
blockToLivevalue for private data? - How would you implement a consent mechanism where the owner of an asset must approve access to its private data?
- 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:
- How would you extend the supply chain solution to include quality control checks at each stage?
- What additional data would you need to track for regulatory compliance in industries like pharmaceuticals or food?
- How would you implement a recall process for defective products?
- What privacy considerations would you need to address in a multi-organization supply chain network?
- How would you integrate IoT devices to automatically update shipment conditions?
Assessment
Multiple Choice Questions:
-
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
-
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
-
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
-
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
-
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:
-
Explain the trade-offs between using private data collections and separate channels for privacy in Hyperledger Fabric.
-
Describe how you would implement a consent mechanism for sharing sensitive data in a healthcare blockchain network.
-
Explain how zero-knowledge proofs can be used to enhance privacy in a Hyperledger Fabric application.
-
Describe the key considerations when integrating Hyperledger Fabric with existing enterprise systems.
-
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:
- A supply chain tracking system for a specific industry (e.g., pharmaceuticals, food, luxury goods)
- A trade finance platform for international trade
- A healthcare data sharing network with privacy controls
- 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.