Learning Hyperledger Fabric — Every Script, Every Line, Every Stage
The goal is: by the end, you can look at a blank server and know exactly what to build, why, in what order, and what every line of every…

Learning Hyperledger Fabric — Every Script, Every Line, Every Stage
The goal is: by the end, you can look at a blank server and know exactly what to build, why, in what order, and what every line of every script is doing and why it exists.
First — What Are We Actually Building?
Before touching any script, you must understand what the final structure is.
A Hyperledger Fabric network is made of:
┌─────────────────────────────────────────────────────┐
│ TRUST LAYER │
│ Certificate Authorities (CA) │
│ → Issue digital identities to everyone │
│ → Without this, nobody can prove who they are │
└─────────────────────────────────────────────────────┘
↓ generates identities for ↓
┌─────────────────────────────────────────────────────┐
│ ORGANIZATION LAYER │
│ Orderer Org + Peer Org (Org1) │
│ → Each org has members with certificates │
│ → Each org has its own MSP folder │
└─────────────────────────────────────────────────────┘
↓ these orgs participate in ↓
┌─────────────────────────────────────────────────────┐
│ NETWORK LAYER │
│ Orderer Node + Peer Node + CouchDB │
│ → Orderer: orders transactions into blocks │
│ → Peer: stores ledger, runs chaincode │
│ → CouchDB: stores current world state │
└─────────────────────────────────────────────────────┘
↓ they communicate on ↓
┌─────────────────────────────────────────────────────┐
│ CHANNEL LAYER │
│ A private ledger shared between specific orgs │
│ → Only orgs on the channel see its transactions │
│ → You can have multiple channels │
└─────────────────────────────────────────────────────┘
↓ logic runs as ↓
┌─────────────────────────────────────────────────────┐
│ CHAINCODE LAYER │
│ Smart contract installed on the channel │
│ → Defines what transactions are allowed │
│ → Runs inside a Docker container on the peer │
└─────────────────────────────────────────────────────┘
These 5 layers must be built in order. You cannot skip or reverse them. Every script maps to exactly one layer.
The Stages Map — Scripts to Blockchain Layers
STAGE 0: Define the rules → configtx.yaml
STAGE 1: Define the containers → docker-compose.yaml + .env
STAGE 2: Start the CAs → docker compose up ca_org1 ca_orderer
STAGE 3: Build all identities → registerEnroll.sh
STAGE 4: Start the network nodes → docker compose up orderer peer0 couchdb0
STAGE 5: Create the channel → createChannel.sh
├── Generate genesis → (configtxgen inside createChannel.sh)
├── Orderer joins → orderer.sh
├── Peer joins → peer channel join
└── Set anchor peer → setAnchorPeer.sh → configUpdate.sh
STAGE 6: Deploy the chaincode → deployCC.sh
├── Package it → packageCC.sh
├── Install it → ccutils.sh → installChaincode()
├── Approve it → ccutils.sh → approveForMyOrg()
├── Check readiness → ccutils.sh → checkCommitReadiness()
└── Commit it → ccutils.sh → commitChaincodeDefinition()
HELPERS (used by every stage above):
envVar.sh → tells peer CLI which org/peer to act as
utils.sh → colored print functions
STAGE 0 — configtx.yaml — The Constitution
What it is: A YAML file that configtxgen reads to produce the channel genesis block. It never runs itself. It is pure configuration. But everything downstream depends on it.
Why it must exist first: Before any node starts, you must define the rules. What organizations exist? What are their policies? What consensus algorithm? This file answers all of that.
Every Section, Every Line
Organizations:
- &OrdererOrg
The &OrdererOrg is a YAML anchor. It means "save this block with the name OrdererOrg so I can reference it later with *OrdererOrg." Without this, you'd have to copy-paste the entire org definition every time you use it.
Name: OrdererOrg
ID: OrdererMSP
ID is the MSP ID. This string is embedded in every certificate issued to this org. When a peer receives a transaction, it reads the certificate's MSP ID to know which org the signer belongs to. This string must match exactly in docker-compose.yaml (ORDERER_GENERAL_LOCALMSPID=OrdererMSP) and in envVar.sh (CORE_PEER_LOCALMSPID=Org1MSP). One typo anywhere = the network rejects every transaction.
MSPDir: ../organizations/ordererOrganizations/example.com/msp
Points to the folder that registerEnroll.sh will generate. This folder contains the CA certificate that signed all orderer identities. configtxgen reads this folder to embed the org's trust anchor into the genesis block.
Policies:
Readers:
Type: Signature
Rule: "OR('OrdererMSP.member')"
Writers:
Type: Signature
Rule: "OR('OrdererMSP.member')"
Admins:
Type: Signature
Rule: "OR('OrdererMSP.admin')"
Policies define who is allowed to do what. There are two types:
Type: Signature= evaluate the actual cryptographic signature on the transaction against the ruleType: ImplicitMeta= "ask all sub-policies and aggregate their results"
OR('OrdererMSP.member') means: "any certificate that was issued by OrdererMSP's CA and has any role (peer, client, admin, orderer) satisfies this rule."
OR('OrdererMSP.admin') means: "only certificates with the admin OU satisfy this rule."
OrdererEndpoints:
- orderer.example.com:7050
This tells the network where to find the orderer. When a peer submits a transaction, it connects to this address. This must match the container name and port in docker-compose.yaml.
- &Org1
...
Policies:
...
Endorsement:
Type: Signature
Rule: "OR('Org1MSP.peer')"
The Endorsement policy is unique to peer orgs (orderers don't endorse). It says: "only a node with OU=peer in its certificate can endorse transactions for Org1." This is how Fabric ensures only legitimate peer nodes — not clients or admins — sign transaction results.
AnchorPeers:
- Host: peer0.org1.example.com
Port: 7051
Defines which peer is the “public face” of Org1 for the gossip protocol. Other orgs’ peers use this address to discover all peers in Org1. If this is wrong, cross-org communication breaks.
Capabilities:
Channel: &ChannelCapabilities
V2_0: true
Orderer: &OrdererCapabilities
V2_0: true
Application: &ApplicationCapabilities
V2_5: true
These are feature flags embedded in the genesis block. When the orderer and peers start up, they read these flags and verify they support them. If you run a Fabric 2.4 peer on a channel with V2_5: true, the peer refuses to participate. This prevents version mismatch bugs from silently corrupting the ledger.
Orderer: &OrdererDefaults
Addresses:
- orderer.example.com:7050
BatchTimeout: 2s
BatchSize:
MaxMessageCount: 500
AbsoluteMaxBytes: 10 MB
PreferredMaxBytes: 2 MB
Block cutting rules. The orderer collects incoming transactions and cuts a new block when:
- 2 seconds have passed since the last block, OR
- 500 transactions have accumulated, OR
- Total size reaches 10MB
PreferredMaxBytes: 2MB is a soft limit — the orderer tries to stay under 2MB but won't reject a valid transaction that pushes it over.
Policies:
BlockValidation:
Type: ImplicitMeta
Rule: "ANY Writers"
BlockValidation is special — it defines who can sign a valid block. ANY Writers means at least one orderer org writer must sign the block. Peers check this signature before accepting a block. If a block has an invalid or missing orderer signature, the peer rejects the entire block.
Profiles:
ChannelUsingRaft:
<<: *ChannelDefaults
Orderer:
<<: *OrdererDefaults
OrdererType: etcdraft
EtcdRaft:
Consenters:
- Host: orderer.example.com
Port: 7050
ClientTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt
ServerTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt
<<: *ChannelDefaults means "inherit all fields from the ChannelDefaults anchor, then override what follows." This is YAML merge syntax.
OrdererType: etcdraft selects the Raft consensus algorithm. Raft requires that each orderer node that participates in consensus is listed as a consenter with its TLS certificates. The TLS certs here are how other orderer nodes verify they're talking to the right node during Raft leader election. With one orderer (your setup), there's no election — it's always the leader.
STAGE 1 — .env and docker-compose.yaml — The Infrastructure Blueprint
How .env works with docker-compose
Every ${VARIABLE} in docker-compose.yaml is replaced by the value from .env at runtime. This separation means:
- One
docker-compose.yamlworks for both local Mac dev and production Linux server - Passwords never hardcoded in compose files
- You switch environments by just pointing to a different
.envfile (NETWORK_ENV_FILE=.env.local ./network.sh up)
The Volume System — Why It Matters
volumes:
ca_org1_data:
peer0.org1.example.com:
couchdb0_data:
Named volumes are managed by Docker, stored on the server’s disk. The critical behaviors:
docker compose down → containers stop, volumes SURVIVE
docker compose down -v → containers stop, volumes DELETED (all data gone)
docker compose up → containers start, volumes RE-ATTACHED (data restored)
This is why network.sh clean uses compose down -v — it's intentionally wiping everything to start fresh. network.sh down uses just compose down — it stops containers but preserves all blockchain data.
The Docker Socket Mount
# In peer0 service:
volumes:
- ${DOCKER_SOCK_PATH}:/var/run/docker.sock
environment:
- CORE_VM_ENDPOINT=unix:///var/run/docker.sock
Why does a peer need Docker access? When chaincode is invoked for the first time, the peer needs to build a Docker image containing your chaincode and start a container for it. The peer does this by talking to the Docker daemon through the socket. Without this mount, chaincode invocations fail silently — the peer can’t build the chaincode container.
Why is the path in .env?
- Mac Docker Desktop: socket is at
/Users/yourname/.docker/run/docker.sock - Linux server: socket is at
/var/run/docker.sock
One file, two environments.
The Network Name Variable
environment:
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=${NETWORK_NAME}
When the peer spawns a chaincode container, it tells Docker: “put this new container on the fabric-did-network network." Without this, the chaincode container starts on the default Docker network — isolated from the peer. The peer sends gRPC calls to the chaincode container to execute transactions, so they must be on the same network. If they're not, every chaincode invocation times out.
STAGE 2 — Starting the CAs
# Inside network.sh up():
compose up -d ca_org1 ca_orderer
Why CAs start first and everything else waits:
The CA generates a root certificate when it first starts. Every subsequent certificate in the network is signed by this root. If you tried to enroll identities before the CA had generated its root cert, you’d get connections refused or invalid cert errors.
waitForCA() {
local name="$1"
local port="$2"
for i in $(seq 1 30); do
if curl -k -s --connect-timeout 2 "https://localhost:${port}/cainfo" >/dev/null 2>&1; then
echo "${name} is ready."
return 0
fi
sleep 2
done
}
Line by line:
seq 1 30→ try up to 30 times (30 × 2 seconds = 60 second timeout)curl -k→-kskips TLS certificate verification. On first startup the CA generates a self-signed cert that curl doesn't trust yet, so we skip verification here-s→ silent mode, no progress output--connect-timeout 2→ don't wait more than 2 seconds per attempt/cainfo→ the CA's health check endpoint. Returns{"result":{"CAName":"ca-org1",...}}when ready>/dev/null 2>&1→ discard all output, we only care about the exit code
STAGE 3 — registerEnroll.sh — Building the Identity System
This is the most important stage. Everything else depends on the output of this script. Let’s trace it completely.
How the script finds its config
ENV_FILE="${NETWORK_ENV_FILE:-$(dirname "$0")/../.env}"
source "${ENV_FILE}"
. "$(dirname "$0")/utils.sh"
${VARIABLE:-default} means "use VARIABLE if set, otherwise use the default value." So if you set NETWORK_ENV_FILE=.env.local before calling the script, it uses .env.local. Otherwise it falls back to .env. This is how the same script works for both test and production.
. "path/to/file" is identical to source "path/to/file" — it loads the file's functions and variables into the current shell session.
ensureCaCerts() — Getting the CA's Root Certificate
docker cp ca_org1:/etc/hyperledger/fabric-ca-server/ca-cert.pem \
"${PRODUCTION_HOME}/organizations/fabric-ca/org1/ca-cert.pem"
Why is this needed? The CA generates its root certificate when it first starts. We need a copy of it on the host machine because every fabric-ca-client command must verify the CA's TLS certificate using --tls.certfiles. Without this file, fabric-ca-client would refuse to connect to the CA with "certificate signed by unknown authority."
The retry loop (for _ in $(seq 1 10)) handles the race condition where the CA just started and hasn't written its cert file yet.
copyFirstFile() — Deterministic File Selection
copyFirstFile() {
local pattern="$1"
local dest="$2"
local matches=()
matches=( $pattern ) # glob expansion → array of matched files
local newest="${matches[0]}"
for f in "${matches[@]}"; do
if [ "${f}" -nt "${newest}" ]; then # -nt = newer than
newest="${f}"
fi
done
cp "${newest}" "${dest}"
}
Why this exists: When fabric-ca-client enroll runs, it creates files in keystore/, signcerts/, tlscacerts/ with randomly generated filenames based on the certificate's hash. If you run enroll twice (e.g., during a re-run), you get two files. The script must pick exactly one. Picking the newest by modification time (-nt) ensures you always get the most recently generated certificate, not a stale one.
registerIfNotExists() — Idempotent Registration
registerIfNotExists() {
local label="$1"
shift # removes $1, so "$@" now = the rest of the command
set +e # temporarily disable "exit on error"
out="$("$@" 2>&1)" # run the registration command, capture ALL output
local rc=$?
set -e # re-enable "exit on error"
if [ $rc -eq 0 ]; then
return 0 # registration succeeded
fi
if echo "$out" | grep -qiE "already registered|Error Code: 74"; then
warnln "${label} already registered (skipping)"
return 0 # already exists = not an error
fi
echo "$out" >&2
return $rc # real error, propagate it
}
Why this exists: If network.sh up is run a second time (e.g., after a partial failure), the CA still has all previously registered identities. Without this wrapper, every fabric-ca-client register would fail with "already registered" and abort the entire script. With it, the script skips existing identities and continues.
"$@" is a special bash variable meaning "all arguments passed to the function." By using shift first (removing the label), "$@" becomes exactly the fabric-ca-client register ... command that was passed in. This is a common bash pattern for wrapping commands.
createOrg1() — The Full Identity Flow
export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/
FABRIC_CA_CLIENT_HOME tells fabric-ca-client where to store all its output files (certificates, keys, configs). Every subsequent fabric-ca-client command writes into this directory. By setting it once here, all operations automatically go to the right place.
Step A: Enroll CA admin
fabric-ca-client enroll \
-u "https://${CA_ORG1_ADMIN_USER}:${CA_ORG1_ADMIN_PASS}@localhost:${CA_ORG1_PORT}" \
--caname ca-org1 \
--tls.certfiles "${PWD}/organizations/fabric-ca/org1/ca-cert.pem"
The -u URL format is https://username:password@host:port. This authenticates with the CA using the bootstrap admin credentials from .env. On success, fabric-ca-client creates:
organizations/peerOrganizations/org1.example.com/
├── msp/
│ ├── cacerts/localhost-7054-ca-org1.pem ← CA's root cert
│ ├── keystore/<hash>_sk ← admin's private key
│ └── signcerts/cert.pem ← admin's signed certificate
Now the admin’s credentials are stored locally and used for all subsequent register operations.
Step B: Write config.yaml (NodeOUs)
echo 'NodeOUs:
Enable: true
ClientOUIdentifier:
Certificate: cacerts/localhost-7054-ca-org1.pem
OrganizationalUnitIdentifier: client
PeerOUIdentifier:
Certificate: cacerts/localhost-7054-ca-org1.pem
OrganizationalUnitIdentifier: peer
AdminOUIdentifier:
Certificate: cacerts/localhost-7054-ca-org1.pem
OrganizationalUnitIdentifier: admin
OrdererOUIdentifier:
Certificate: cacerts/localhost-7054-ca-org1.pem
OrganizationalUnitIdentifier: orderer' > "${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml"
This is the role mapping file. When Fabric evaluates a policy like OR('Org1MSP.peer'), it reads this file to understand: "a certificate with OU=peer embedded in it = a peer role." Without this file, Fabric cannot distinguish between a peer, client, and admin — all would be treated the same, and endorsement policy checks would fail.
Step C: Register identities
fabric-ca-client register \
--caname ca-org1 \
--id.name peer0 \
--id.secret "${PEER0_PW}" \
--id.type peer \
--tls.certfiles "${PWD}/organizations/fabric-ca/org1/ca-cert.pem"
--id.type peer tells the CA to embed OU=peer in any certificate issued for this identity. This is what makes the Endorsement policy OR('Org1MSP.peer') work — without --id.type peer, the peer0 certificate won't have the peer OU and the peer would fail to endorse transactions.
fabric-ca-client register \
--id.name issuer1 \
--id.type client \
--id.attrs "role=issuer:ecert" \ # ← custom attribute
--id.attrs "role=issuer:ecert" embeds a custom attribute role=issuer into the certificate itself (:ecert means "put it in the enrollment cert"). Your chaincode can then read this attribute using ctx.GetClientIdentity().GetAttributeValue("role") and enforce role-based access control at the chaincode level without any external database.
Step D: Enroll peer0 identity cert
fabric-ca-client enroll \
-u "https://peer0:${PEER0_PW}@localhost:${CA_ORG1_PORT}" \
--caname ca-org1 \
-M "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp" \
--tls.certfiles "${PWD}/organizations/fabric-ca/org1/ca-cert.pem"
-M specifies the output MSP directory. This is the peer0-specific identity cert folder. The docker-compose.yaml mounts this exact folder into the peer container:
- ../organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp
Step E: Enroll peer0 TLS cert (separate enrollment)
fabric-ca-client enroll \
-u "https://peer0:${PEER0_PW}@localhost:${CA_ORG1_PORT}" \
--caname ca-org1 \
-M "${PWD}/.../peers/peer0.org1.example.com/tls" \
--enrollment.profile tls \
--csr.hosts peer0.org1.example.com \
--csr.hosts localhost \
--tls.certfiles "${PWD}/organizations/fabric-ca/org1/ca-cert.pem"
Why a second enrollment for the same identity?
- First enrollment → identity certificate (who you ARE — your MSP cert)
- Second enrollment → TLS certificate (encrypting the WIRE — network traffic)
--enrollment.profile tls tells the CA to issue a TLS-type certificate instead of an identity certificate. --csr.hosts lists hostnames this TLS cert is valid for. If a client connects to peer0 and the hostname doesn't match the cert's SANs, TLS handshake fails. Both peer0.org1.example.com (Docker network name) and localhost (for CLI tools running on host) must be listed.
Step F: Copy TLS certs to well-known names
copyFirstFile "${PWD}/.../tls/signcerts/*" "${PWD}/.../tls/server.crt"
copyFirstFile "${PWD}/.../tls/keystore/*" "${PWD}/.../tls/server.key"
copyFirstFile "${PWD}/.../tls/tlscacerts/*" "${PWD}/.../tls/ca.crt"
fabric-ca-client generates files with hash-based names. docker-compose.yaml mounts them as server.crt, server.key, ca.crt. These copies bridge that gap. The orderer container references:
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
So server.crt must exist at that exact name.
STAGE 4 — Starting the Network Nodes
compose up -d couchdb0 orderer.example.com peer0.org1.example.com
These three containers can now start because registerEnroll.sh has populated all the certificate folders they need. Docker mounts those folders into the containers at startup.
The dependency chain:
couchdb0 → no dependencies, starts first
orderer → needs orderer MSP + TLS certs (now exist)
peer0 → needs peer MSP + TLS certs + couchdb (depends_on: couchdb0, orderer)
depends_on in docker-compose ensures start ORDER, but NOT readiness. The peer may start before couchdb is fully initialized. This is why createChannel.sh has retry loops — it waits for the network to stabilize.
STAGE 5 — createChannel.sh — 4 Functions in Order
createChannelGenesisBlock()
export FABRIC_CFG_PATH="$(cd "$(dirname "$0")/../configtx" && pwd)"
FABRIC_CFG_PATH tells configtxgen where to find configtx.yaml. This is the one variable configtxgen absolutely requires. Without it, configtxgen looks in the current directory and fails.
configtxgen \
-profile ChannelUsingRaft \
-outputBlock ./channel-artifacts/${CHANNEL_NAME}.block \
-channelID ${CHANNEL_NAME}
-profile ChannelUsingRaft matches the profile name in configtx.yaml exactly. configtxgen reads that profile, merges all the defaults (using <<: merge keys), resolves all *Anchors, reads the MSP folders referenced in MSPDir, and encodes everything into a binary protobuf file — the genesis block.
The genesis block contains:
- All org MSP definitions (their CA certs)
- All channel policies
- All orderer configuration (Raft consenters, batch settings)
- Channel capabilities
- Orderer endpoints
This single file IS the channel’s identity. Every node that joins uses it as the source of truth.
createChannel() — Orderer Joins
bash "$(dirname "$0")/orderer.sh" "${CHANNEL_NAME}"
This calls orderer.sh which runs:
osnadmin channel join \
--channelID "${CHANNEL_NAME}" \
--config-block "./channel-artifacts/${CHANNEL_NAME}.block" \
-o localhost:7053 \
--ca-file "${ORDERER_CA}" \
--client-cert "${ORDERER_ADMIN_TLS_SIGN_CERT}" \
--client-key "${ORDERER_ADMIN_TLS_PRIVATE_KEY}"
Port 7053 vs 7050:
7050= the transaction port. Peers use this to submit transactions7053= the admin port. Onlyosnadminuses this to manage channel membership
--ca-file → the orderer's TLS CA cert (to verify we're talking to the real orderer) --client-cert + --client-key → mutual TLS. The orderer verifies our client cert to confirm we're an authorized admin. This is why orderer admin enrollment must happen before this step.
Idempotent handling:
if grep -qiE "channel already exists|Status: 405" "${tmp_out}"; then
_return_or_exit 0 # already joined = success, not error
fi
Status: 405 is what the orderer returns when you try to join a channel it's already on. Treating this as success makes the script safe to re-run.
joinChannel() — Peer Joins
export FABRIC_CFG_PATH="$(cd "$(dirname "$0")/../config" && pwd)"
setGlobals $ORG
peer channel join -b $BLOCKFILE
FABRIC_CFG_PATH switches from configtx/ to config/ — this is because configtxgen needs the channel profile config, but the peer binary needs core.yaml (the peer's operational config). Both use FABRIC_CFG_PATH but look for different files.
setGlobals 1 (from envVar.sh) sets:
CORE_PEER_LOCALMSPID=Org1MSP
CORE_PEER_ADDRESS=localhost:7051
CORE_PEER_MSPCONFIGPATH=.../users/Admin@org1.example.com/msp
CORE_PEER_TLS_ROOTCERT_FILE=.../tlsca/tlsca.org1.example.com-cert.pem
The peer CLI binary reads these env vars to know: which org to act as, which peer to connect to, whose credentials to use, which TLS cert to trust.
peer channel join -b $BLOCKFILE sends the genesis block to peer0. The peer verifies the block's signatures, creates a local copy of the ledger, and connects to the orderer to start receiving new blocks.
Already joined handling:
if grep -qiE "ledger \[${CHANNEL_NAME}\] already exists" log.txt; then
res=0 # not an error
fi
setAnchorPeer() — The Channel Config Update Flow
This is the most complex part of channel creation. Understanding it teaches you the general pattern for ALL channel config updates — adding new orgs, changing policies, updating orderer settings.
Step A: Fetch current channel config
# configUpdate.sh → fetchChannelConfig()
peer channel fetch config "${PRODUCTION_HOME}/channel-artifacts/config_block.pb" \
-o localhost:7050 \
--ordererTLSHostnameOverride orderer.example.com \
-c "$CHANNEL" \
--tls --cafile "$ORDERER_CA"
peer channel fetch config fetches the latest config block from the orderer. Unlike data blocks (which contain transactions), config blocks contain only channel configuration. The orderer sends back a binary protobuf file.
--ordererTLSHostnameOverride orderer.example.com — when connecting to localhost:7050, the TLS certificate says orderer.example.com. Without this override, TLS validation would fail because the hostname localhost doesn't match orderer.example.com in the cert.
configtxlator proto_decode \
--input "config_block.pb" \
--type common.Block \
--output "config_block.json"
configtxlator is a Swiss Army knife for Fabric config. proto_decode converts binary protobuf → human-readable JSON. --type common.Block tells it the schema to use for decoding.
jq .data.data[0].payload.data.config "config_block.json" > "Org1MSPconfig.json"
jq is a JSON query tool. This navigates the deeply nested config block structure and extracts just the channel config object (discarding the block header and signatures). The path .data.data[0].payload.data.config is the standard Fabric config block structure — memorize this path, it's always the same.
Step B: Modify the config
jq '.channel_group.groups.Application.groups.'${CORE_PEER_LOCALMSPID}'.values +=
{"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "'$HOST'","port": '$PORT'}]},"version": "0"}}' \
"Org1MSPconfig.json" > "Org1MSPmodified_config.json"
This jq command navigates to Org1MSP's values within the channel config and adds a new AnchorPeers entry. The structure must exactly match Fabric's protobuf schema — wrong field names are silently ignored and the anchor peer won't actually be set.
mod_policy: "Admins" means only admins can modify the anchor peer config in future updates.
Step C: Compute the config UPDATE (not the full config)
# Encode both configs to binary protobuf
configtxlator proto_encode --input "Org1MSPconfig.json" \
--type common.Config --output "original_config.pb"
configtxlator proto_encode --input "Org1MSPmodified_config.json" \
--type common.Config --output "modified_config.pb"
# Compute ONLY the difference
configtxlator compute_update \
--channel_id "${CHANNEL}" \
--original "original_config.pb" \
--updated "modified_config.pb" \
--output "config_update.pb"
Why compute only the diff? Because channel config updates require approval from multiple orgs (based on the mod_policy). By submitting only the delta, each approving org signs only what’s changing, not the entire config. This also prevents race conditions — if two orgs submit updates simultaneously, the orderer can apply both safely.
# Decode diff back to JSON
configtxlator proto_decode --input "config_update.pb" \
--type common.ConfigUpdate --output "config_update.json"
# Wrap in an envelope (required by Fabric protocol)
echo '{"payload":{"header":{"channel_header":{"channel_id":"'${CHANNEL}'","type":2}},"data":{"config_update":'$(cat "config_update.json)"'}}}' \
| jq . > "config_update_in_envelope.json"
# Encode envelope to binary
configtxlator proto_encode \
--input "config_update_in_envelope.json" \
--type common.Envelope \
--output "Org1MSPanchors.tx"
The envelope wrapper is required by Fabric’s message format. "type":2 means this is a CONFIG_UPDATE message type (as defined in Fabric's protobuf schema). Without the envelope, peer channel update rejects the transaction.
Step D: Submit the config update
peer channel update \
-o localhost:7050 \
--ordererTLSHostnameOverride orderer.example.com \
-c "$CHANNEL_NAME" \
-f "Org1MSPanchors.tx" \
--tls --cafile "$ORDERER_CA"
This submits the config update transaction to the orderer. The orderer validates that the signatures on the envelope satisfy the channel’s Admins policy (since mod_policy: "Admins"). If valid, the orderer creates a new config block and appends it to the channel. All peers that are subscribed to the channel receive this new config block and update their local view of the channel configuration.
STAGE 6 — deployCC.sh + Supporting Scripts
packageCC.sh — Creating the Deployable Unit
CC_SRC_LANGUAGE=$(echo "$CC_SRC_LANGUAGE" | tr [:upper:] [:lower:])
Normalizes to lowercase so Go, GO, go all work.
if [ "$CC_SRC_LANGUAGE" = "go" ]; then
CC_RUNTIME_LANGUAGE=golang
if [ "${SKIP_GO_VENDOR:-false}" = "true" ]; then
if [ -d "${CC_SRC_PATH}/vendor" ]; then
rm -rf "${CC_SRC_PATH}/vendor"
fi
else
pushd $CC_SRC_PATH
GO111MODULE=on go mod vendor
popd
fi
go mod vendor downloads all Go dependencies into a vendor/ folder inside the chaincode directory. This makes the package self-contained — when the peer builds the chaincode Docker image, it doesn't need internet access to download dependencies. GO111MODULE=on forces Go modules mode.
SKIP_GO_VENDOR=true skips vendoring (faster for development when deps haven't changed). If set, it also removes existing vendor/ to reduce Docker build context size (important on Docker Desktop which has slow file I/O).
pushd/popd — pushd changes directory and saves the previous directory to a stack. popd restores it. Used here to run go mod vendor inside the chaincode folder, then return to the original directory.
packageChaincode() {
peer lifecycle chaincode package ${CC_NAME}.tar.gz \
--path ${CC_SRC_PATH} \
--lang ${CC_RUNTIME_LANGUAGE} \
--label ${CC_NAME}_${CC_VERSION}
}
peer lifecycle chaincode package creates a .tar.gz containing:
metadata.json(chaincode name, version, language)code.tar.gz(your source code + vendor/)
--label is a human-readable tag. The package ID is label:sha256(package_contents). The SHA256 ensures two packages with the same label but different code get different IDs — critical for detecting tampering.
ccutils.sh — The Lifecycle Functions
installChaincode()
function installChaincode() {
ORG=$1
setGlobals $ORG
peer lifecycle chaincode queryinstalled --output json | \
jq -r 'try (.installed_chaincodes[].package_id)' | \
grep ^${PACKAGE_ID}$
if test $? -ne 0; then
peer lifecycle chaincode install ${CC_NAME}.tar.gz
fi
}
Idempotency check: Before installing, query what’s already installed and grep for our package ID. jq -r 'try (...)' — try means "if this path doesn't exist, return null instead of error." -r means "raw output" (no JSON quotes). If our package ID is already there (grep finds it), skip installation. This is safe to re-run.
peer lifecycle chaincode install copies the .tar.gz to the peer's local filesystem. The peer doesn't build the chaincode Docker image yet — that happens on first invocation.
approveForMyOrg()
peer lifecycle chaincode approveformyorg \
-o localhost:7050 \
--ordererTLSHostnameOverride orderer.example.com \
--tls --cafile "$ORDERER_CA" \
--channelID $CHANNEL_NAME \
--name ${CC_NAME} \
--version ${CC_VERSION} \
--package-id ${PACKAGE_ID} \
--sequence ${CC_SEQUENCE}
What “approve” means on-chain: This submits a transaction to the orderer that records “Org1MSP approves chaincode basic v1.0 with package ID X at sequence 1.” This approval is stored IN the blockchain ledger (in the lifecycle system chaincode namespace). Other orgs (and the commit step) can query this record.
--sequence is a monotonically increasing integer. First deployment = 1. If you upgrade the chaincode, you increment to 2. This prevents replay attacks where someone re-approves an old chaincode definition.
--package-id links the abstract definition (name, version, policy) to the specific compiled package. Different orgs can approve with different package IDs (if they compiled from the same source independently) — this is how Fabric supports decentralized builds.
checkCommitReadiness()
peer lifecycle chaincode checkcommitreadiness \
--channelID $CHANNEL_NAME \
--name ${CC_NAME} \
--version ${CC_VERSION} \
--sequence ${CC_SEQUENCE} \
--output json
# Returns: {"approvals": {"Org1MSP": true}}
This queries the orderer/peers to see which organizations have approved. The commit step requires that the approval count satisfies the channel’s LifecycleEndorsement policy. In your network with one org and MAJORITY Endorsement policy: 1 org, majority = 1, so only Org1 needs to approve.
for var in "$@"; do
grep "$var" log.txt &>/dev/null || let rc=1
done
The function is called as checkCommitReadiness 1 "\"Org1MSP\": true". It loops through all expected approval strings and checks that each appears in the output. If any org is missing, it retries up to MAX_RETRY times — the approval transaction might not have propagated to all peers yet.
commitChaincodeDefinition()
parsePeerConnectionParameters $@ # builds PEER_CONN_PARMS array
peer lifecycle chaincode commit \
-o localhost:7050 \
--ordererTLSHostnameOverride orderer.example.com \
--tls --cafile "$ORDERER_CA" \
--channelID $CHANNEL_NAME \
--name ${CC_NAME} \
--version ${CC_VERSION} \
--sequence ${CC_SEQUENCE} \
"${PEER_CONN_PARMS[@]}"
parsePeerConnectionParameters (from envVar.sh) builds a --peerAddresses localhost:7051 --tlsRootCertFiles /path/to/ca.crt array for every org passed in. The commit transaction must be endorsed by peers that have the chaincode installed, so the orderer needs to know which peers to collect endorsements from.
The commit transaction is sent to the orderer, which collects endorsements from the listed peers, verifies the approval count satisfies LifecycleEndorsement policy, then writes a config update to the channel recording the chaincode as active. After this block is committed, any peer on the channel can invoke the chaincode.
envVar.sh — Why It Exists and How It Works
setGlobals() {
local USING_ORG=""
if [ -z "${OVERRIDE_ORG:-}" ]; then
USING_ORG=$1
else
USING_ORG="${OVERRIDE_ORG}"
fi
OVERRIDE_ORG is an escape hatch — if set, it overrides the argument passed to setGlobals. Useful for debugging: OVERRIDE_ORG=2 ./network.sh deployCC would run all peer operations as Org2 even if the scripts pass 1.
if [ $USING_ORG -eq 1 ]; then
export CORE_PEER_LOCALMSPID=Org1MSP
export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA
export CORE_PEER_MSPCONFIGPATH=${PRODUCTION_HOME}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
fi
These 4 variables are everything the peer binary needs:
LOCALMSPID→ which org's identity to use when signingTLS_ROOTCERT_FILE→ which CA cert to use to verify the peer's TLS certificateMSPCONFIGPATH→ where to find the signing key and certificate (admin's MSP folder)ADDRESS→ which peer to connect to
When you add Org2, you’d add an elif [ $USING_ORG -eq 2 ] block with Org2's values. Existing scripts automatically work with the second org just by calling setGlobals 2.
utils.sh — Why Even This Matters
C_RESET='\033[0m'
C_RED='\033[0;31m'
C_GREEN='\033[0;32m'
function fatalln() {
errorln "$1"
exit 1 # ← exits the ENTIRE script, not just the function
}
export -f errorln
export -f successln
export -f infoln
export -f warnln
export -f exports functions to subshells. When createChannel.sh does bash scripts/orderer.sh, that spawns a new bash process. Without export -f, orderer.sh wouldn't have access to fatalln, infoln, etc. The export makes them available to every child script automatically.
The Mental Model — Putting It All Together
QUESTION: "Why can't I skip Stage 3 and start the peer directly?"
ANSWER: peer container mounts ../organizations/peerOrganizations/org1.example.com/peers/peer0.../msp
That folder doesn't exist until registerEnroll.sh runs.
Docker mounts a non-existent folder as an empty directory.
Peer starts but has no MSP identity → rejects all connections.
QUESTION: "Why can't I run createChannel before starting the peer?"
ANSWER: peer channel join connects to CORE_PEER_ADDRESS=localhost:7051
If peer0 container isn't running, connection refused.
But genesis block creation (configtxgen) CAN run before peers start.
QUESTION: "Why does deployCC need the channel to exist?"
ANSWER: approveformyorg writes to the channel ledger.
No channel = no ledger = nowhere to write the approval.
QUESTION: "Why does the peer need the Docker socket?"
ANSWER: First chaincode invoke → peer builds Docker image from chaincode package.
Without Docker socket, peer can't create the chaincode container.
Every invoke fails with "failed to launch chaincode".
QUESTION: "Why does setAnchorPeer happen after joinChannel, not before?"
ANSWER: setAnchorPeer fetches the current channel config from the orderer.
The peer must be joined first to be authorized to fetch config.
Also, the anchor peer must exist (be joined) before being declared.
What You Can Now Do
After deeply understanding all of this, you can:
- Explain every line of every script without looking anything up
- Debug failures — if something breaks, you know exactly which stage it’s in and why
- Add a second org — you know exactly what to add to
configtx.yaml, whatregisterEnroll.shneeds to duplicate, whatenvVar.shneeds forsetGlobals 2, and what the config update flow looks like - Upgrade chaincode — increment
CHAINCODE_SEQUENCE, re-rundeployCC.sh - Write these scripts from scratch — starting from
configtx.yaml→registerEnroll.sh→createChannel.sh→deployCC.shin that exact order
The replication step is now just confirming what you already know.
메타데이터
- post_id
- fc96746ffcd6
- slug
- learning-hyperledger-fabric-every-script-every-line-every-stage-fc96746ffcd6
- url
- https://coinsbench.com/learning-hyperledger-fabric-every-script-every-line-every-stage-fc96746ffcd6
- canonical_url
- https://coinsbench.com/learning-hyperledger-fabric-every-script-every-line-every-stage-fc96746ffcd6
- author_url
- https://medium.com/@muhammadtalha1
- status
- ok
- fetched_at
- 2026-06-09 15:37:30