Set up the Blockchain Network

Set up the Blockchain Network

Step 1: Open Docker Desktop, to let the Docker Daemon runs

Once you click open the software, Docker Daemon is already running. No further actions needed.

Step 2: Clone the Hyperledger Fabric Sample repository, pull down the Docker images, and download the platform-specific binary

Determine a directory you want the sample repo to be in and run:

curl -sSL https://bit.ly/2ysbOFE | bash -s

The curl command will perform clone, pull down, download, and show result in both Terminal and Docker Desktop:

Terminal

Clone hyperledger/fabric-samples repo


===> Cloning hyperledger/fabric-samples repo

Cloning into 'fabric-samples'...

remote: Enumerating objects: 12075, done.

remote: Counting objects: 100% (115/115), done.

remote: Compressing objects: 100% (82/82), done.

remote: Total 12075 (delta 31), reused 101 (delta 27), pack-reused 11960

Receiving objects: 100% (12075/12075), 22.19 MiB | 16.22 MiB/s, done.

Resolving deltas: 100% (6476/6476), done.

===> Checking out v2.4.9 of hyperledger/fabric-samples


Pull Hyperledger Fabric binaries


===> Downloading version 2.4.9 platform specific fabric binaries

===> Downloading: https://github.com/hyperledger/fabric/releases/download/v2.4.9/hyperledger-fabric-darwin-amd64-2.4.9.tar.gz

% Total % Received % Xferd Average Speed Time Time Time Current

Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0

100 75.4M 100 75.4M 0 0 18.7M 0 0:00:04 0:00:04 --:--:-- 32.3M

==> Done.

===> Downloading version 1.5.5 platform specific fabric-ca-client binary

===> Downloading: https://github.com/hyperledger/fabric-ca/releases/download/v1.5.5/hyperledger-fabric-ca-darwin-amd64-1.5.5.tar.gz

% Total % Received % Xferd Average Speed Time Time Time Current

Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0

100 29.3M 100 29.3M 0 0 14.8M 0 0:00:01 0:00:01 --:--:-- 41.7M

==> Done.


Pull Hyperledger Fabric docker images


FABRIC_IMAGES: peer orderer ccenv tools baseos

===> Pulling fabric Images

====> docker.io/hyperledger/fabric-peer:2.4.9

2.4.9: Pulling from hyperledger/fabric-peer

ef5531b6e74e: Pull complete

cb6adad689bd: Pull complete

010602cca7ea: Pull complete

dadcffad5b31: Pull complete

3b71f3b82217: Pull complete

9917d15b0b5a: Pull complete

6a2c93fa3f95: Pull complete

Digest: sha256:6ff36af21eb1e0b74f09187a6090c27c42d82b33aa0404bcf94558217e1dfba2

Status: Downloaded newer image for hyperledger/fabric-peer:2.4.9

docker.io/hyperledger/fabric-peer:2.4.9

====> docker.io/hyperledger/fabric-orderer:2.4.9

2.4.9: Pulling from hyperledger/fabric-orderer

ef5531b6e74e: Already exists

cb6adad689bd: Already exists

010602cca7ea: Already exists

44d81e4543ec: Pull complete

9c0a7cb5c6af: Pull complete

275371067d0a: Pull complete

d90823f8c566: Pull complete

Digest: sha256:6ec3fe59ea55b690aaab6c69e5e23da5dc99afda2f046116ed296b395e4d47d1

Status: Downloaded newer image for hyperledger/fabric-orderer:2.4.9

docker.io/hyperledger/fabric-orderer:2.4.9

====> docker.io/hyperledger/fabric-ccenv:2.4.9

2.4.9: Pulling from hyperledger/fabric-ccenv

ca7dd9ec2225: Pull complete

c41ae7ad2b39: Pull complete

eda8cd824576: Pull complete

d71a49e0649a: Pull complete

d34b692b3ee4: Pull complete

66044e0a0724: Pull complete

d974a502e7a7: Pull complete

4fc4ce450dbd: Pull complete

Digest: sha256:b23821060701e9713c4714f337f691b0a736a9312a9360e0886f3acf1df210bc

Status: Downloaded newer image for hyperledger/fabric-ccenv:2.4.9

docker.io/hyperledger/fabric-ccenv:2.4.9

====> docker.io/hyperledger/fabric-tools:2.4.9

2.4.9: Pulling from hyperledger/fabric-tools

ca7dd9ec2225: Already exists

c41ae7ad2b39: Already exists

eda8cd824576: Already exists

d71a49e0649a: Already exists

e918dcca351a: Pull complete

8c49fb870a8c: Pull complete

aa900656510d: Pull complete

Digest: sha256:b1194f509085e0fe622355d119fbb8dfef8b2d78954eabd7d221a39fc90bb850

Status: Downloaded newer image for hyperledger/fabric-tools:2.4.9

docker.io/hyperledger/fabric-tools:2.4.9

====> docker.io/hyperledger/fabric-baseos:2.4.9

2.4.9: Pulling from hyperledger/fabric-baseos

ef5531b6e74e: Already exists

cb6adad689bd: Already exists

bed8f466ba67: Pull complete

Digest: sha256:b86dc8040a48c4dc9df5a9cb309a40ffaaf3984f5cbfe555c00ecf4d02c52ef9

Status: Downloaded newer image for hyperledger/fabric-baseos:2.4.9

docker.io/hyperledger/fabric-baseos:2.4.9

===> Pulling fabric ca Image

====> docker.io/hyperledger/fabric-ca:1.5.5

1.5.5: Pulling from hyperledger/fabric-ca

2408cc74d12b: Pull complete

979557f40538: Pull complete

b3bcd28c0311: Pull complete

Digest: sha256:f93cd9f32702c3a6b9cb305d75bed5edd884cae0674374fd7c26467bf6a0ed9b

Status: Downloaded newer image for hyperledger/fabric-ca:1.5.5

docker.io/hyperledger/fabric-ca:1.5.5

===> List out hyperledger docker images

hyperledger/fabric-tools 2.4 6997a3225d8b 13 days ago 490MB

hyperledger/fabric-tools 2.4.9 6997a3225d8b 13 days ago 490MB

hyperledger/fabric-tools latest 6997a3225d8b 13 days ago 490MB

hyperledger/fabric-peer 2.4 b1c5352410a0 13 days ago 64.2MB

hyperledger/fabric-peer 2.4.9 b1c5352410a0 13 days ago 64.2MB

hyperledger/fabric-peer latest b1c5352410a0 13 days ago 64.2MB

hyperledger/fabric-orderer 2.4 d64956fd66bb 13 days ago 36.7MB

hyperledger/fabric-orderer 2.4.9 d64956fd66bb 13 days ago 36.7MB

hyperledger/fabric-orderer latest d64956fd66bb 13 days ago 36.7MB

hyperledger/fabric-ccenv 2.4 7557f03b34a9 13 days ago 521MB

hyperledger/fabric-ccenv 2.4.9 7557f03b34a9 13 days ago 521MB

hyperledger/fabric-ccenv latest 7557f03b34a9 13 days ago 521MB

hyperledger/fabric-baseos 2.4 0792904ee001 13 days ago 6.8MB

hyperledger/fabric-baseos 2.4.9 0792904ee001 13 days ago 6.8MB

hyperledger/fabric-baseos latest 0792904ee001 13 days ago 6.8MB

hyperledger/fabric-ca 1.5 93f19fa873cb 8 months ago 76.5MB

hyperledger/fabric-ca 1.5.5 93f19fa873cb 8 months ago 76.5MB

hyperledger/fabric-ca latest 93f19fa873cb 8 months ago 76.5MB

Docker Desktop

Hyperledger Fabric Sample repo

Step 3: Add the bin sub-directory into the PATH environment variable

The curl command executed in step 2 downloaded and executed a bash script that  download and extract all of the platform-specific binaries you will need to set up your network and place them into the bin sub-directory. There are ten binaries:

You should add the sub-directory to your PATH environment variable so that these can be picked up without fully qualifying the path to each binary. E.g.:

export PATH={Your fabric-samples repo}/bin:$PATH

Step 4: Bring up the network

Go into the test-network sub-directory, run:

cd fabric-samples/test-network

Then, execute another command with the network.sh script:

./network.sh up

This command creates a Fabric network that consists of two peer nodes and one ordering node. The result will be shown in both Terminal and Docker Desktop:

Terminal

Using docker and docker-compose

Starting nodes with CLI timeout of '5' tries and CLI delay of '3' seconds and using database 'leveldb' with crypto from 'cryptogen'

LOCAL_VERSION=2.4.9

DOCKER_IMAGE_VERSION=2.4.9

/Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/../bin/cryptogen

Generating certificates using cryptogen tool

Creating Org1 Identities

+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org1.yaml --output=organizations

org1.example.com

+ res=0

Creating Org2 Identities

+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org2.yaml --output=organizations

org2.example.com

+ res=0

Creating Orderer Org Identities

+ cryptogen generate --config=./organizations/cryptogen/crypto-config-orderer.yaml --output=organizations

+ res=0

Generating CCP files for Org1 and Org2

[+] Running 8/8

 ⠿ Network fabric_test Created 0.1s

 ⠿ Volume "compose_peer0.org2.example.com" Created 0.0s

 ⠿ Volume "compose_orderer.example.com" Created 0.0s

 ⠿ Volume "compose_peer0.org1.example.com" Created 0.0s

 ⠿ Container peer0.org1.example.com Started 2.7s

 ⠿ Container orderer.example.com Started 2.6s

 ⠿ Container peer0.org2.example.com Started 2.8s

 ⠿ Container cli Started 3.3s

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

696ec5c8f5d7 hyperledger/fabric-tools:latest "/bin/bash" 3 seconds ago Up Less than a second cli

a86559e93f4a hyperledger/fabric-orderer:latest "orderer" 4 seconds ago Up 1 second 0.0.0.0:7050->7050/tcp, 0.0.0.0:7053->7053/tcp, 0.0.0.0:9443->9443/tcp orderer.example.com

d484f2f81ad0 hyperledger/fabric-peer:latest "peer node start" 4 seconds ago Up 1 second 0.0.0.0:7051->7051/tcp, 0.0.0.0:9444->9444/tcp peer0.org1.example.com

886fa770ac6f hyperledger/fabric-peer:latest "peer node start" 4 seconds ago Up 1 second 0.0.0.0:9051->9051/tcp, 7051/tcp, 0.0.0.0:9445->9445/tcp peer0.org2.example.com

Docker Desktop

Step 5: Create a channel

Channel is a private layer of communication between specific network members. Channels can be used only by organizations that are invited to the channel, and are invisible to other members of the network. Each channel has a separate blockchain ledger. Organizations that have been invited to join their peers to the channel to store the channel ledger and validate the transactions on the channel.

Now that you have peer and orderer nodes running on your machine, you can use the script to create a channel for transactions between Org1 and Org2.

Let's execute another command with the network.sh script:

./network.sh createChannel

The channel created with the default name 'my channel' will be presented on both Terminal and Docker Desktop:

Terminal

Using docker and docker-compose

Creating channel 'mychannel'.

If network is not up, starting nodes with CLI timeout of '5' tries and CLI delay of '3' seconds and using database 'leveldb

Network Running Already

Using docker and docker-compose

Generating channel genesis block 'mychannel.block'

/Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/../bin/configtxgen

+ configtxgen -profile TwoOrgsApplicationGenesis -outputBlock ./channel-artifacts/mychannel.block -channelID mychannel

2023-03-16 12:56:26.465 HKT 0001 INFO [common.tools.configtxgen] main -> Loading configuration

2023-03-16 12:56:26.479 HKT 0002 INFO [common.tools.configtxgen.localconfig] completeInitialization -> orderer type: etcdraft

2023-03-16 12:56:26.479 HKT 0003 INFO [common.tools.configtxgen.localconfig] completeInitialization -> Orderer.EtcdRaft.Options unset, setting to tick_interval:"500ms" election_tick:10 heartbeat_tick:1 max_inflight_blocks:5 snapshot_interval_size:16777216

2023-03-16 12:56:26.479 HKT 0004 INFO [common.tools.configtxgen.localconfig] Load -> Loaded configuration: /Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/configtx/configtx.yaml

2023-03-16 12:56:26.487 HKT 0005 INFO [common.tools.configtxgen] doOutputBlock -> Generating genesis block

2023-03-16 12:56:26.487 HKT 0006 INFO [common.tools.configtxgen] doOutputBlock -> Creating application channel genesis block

2023-03-16 12:56:26.488 HKT 0007 INFO [common.tools.configtxgen] doOutputBlock -> Writing genesis block

+ res=0

Creating channel mychannel

Using organization 1

+ osnadmin channel join --channelID mychannel --config-block ./channel-artifacts/mychannel.block -o localhost:7053 --ca-file /Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem --client-cert /Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt --client-key /Users/aerobot/Documents/Sandbox/ChainWallet/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.key

+ res=0

Status: 201

{

 "name": "mychannel",

 "url": "/participation/v1/channels/mychannel",

 "consensusRelation": "consenter",

 "status": "active",

 "height": 1

}


Channel 'mychannel' created

Joining org1 peer to the channel...

Using organization 1

+ peer channel join -b ./channel-artifacts/mychannel.block

+ res=0

2023-03-16 12:56:33.199 HKT 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 12:56:33.235 HKT 0002 INFO [channelCmd] executeJoin -> Successfully submitted proposal to join channel

Joining org2 peer to the channel...

Using organization 2

+ peer channel join -b ./channel-artifacts/mychannel.block

+ res=0

2023-03-16 12:56:36.322 HKT 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 12:56:36.356 HKT 0002 INFO [channelCmd] executeJoin -> Successfully submitted proposal to join channel

Setting anchor peer for org1...

Using organization 1

Fetching channel config for channel mychannel

Using organization 1

Fetching the most recent configuration block for the channel

+ peer channel fetch config config_block.pb -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com -c mychannel --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem

2023-03-16 04:56:36.719 UTC 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 04:56:36.723 UTC 0002 INFO [cli.common] readBlock -> Received block: 0

2023-03-16 04:56:36.723 UTC 0003 INFO [channelCmd] fetch -> Retrieving last config block: 0

2023-03-16 04:56:36.725 UTC 0004 INFO [cli.common] readBlock -> Received block: 0

Decoding config block to JSON and isolating config to Org1MSPconfig.json

+ configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json

+ jq '.data.data[0].payload.data.config' config_block.json

Generating anchor peer update transaction for Org1 on channel mychannel

+ jq '.channel_group.groups.Application.groups.Org1MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org1.example.com","port": 7051}]},"version": "0"}}' Org1MSPconfig.json

+ 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

+ configtxlator compute_update --channel_id mychannel --original original_config.pb --updated modified_config.pb --output config_update.pb

+ configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json

+ jq .

++ cat config_update.json

+ echo '{"payload":{"header":{"channel_header":{"channel_id":"mychannel", "type":2}},"data":{"config_update":{' '"channel_id":' '"mychannel",' '"isolated_data":' '{},' '"read_set":' '{' '"groups":' '{' '"Application":' '{' '"groups":' '{' '"Org1MSP":' '{' '"groups":' '{},' '"mod_policy":' '"",' '"policies":' '{' '"Admins":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Endorsement":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Readers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Writers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '}' '},' '"values":' '{' '"MSP":' '{' '"mod_policy":' '"",' '"value":' null, '"version":' '"0"' '}' '},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '},' '"write_set":' '{' '"groups":' '{' '"Application":' '{' '"groups":' '{' '"Org1MSP":' '{' '"groups":' '{},' '"mod_policy":' '"Admins",' '"policies":' '{' '"Admins":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Endorsement":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Readers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Writers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '}' '},' '"values":' '{' '"AnchorPeers":' '{' '"mod_policy":' '"Admins",' '"value":' '{' '"anchor_peers":' '[' '{' '"host":' '"peer0.org1.example.com",' '"port":' 7051 '}' ']' '},' '"version":' '"0"' '},' '"MSP":' '{' '"mod_policy":' '"",' '"value":' null, '"version":' '"0"' '}' '},' '"version":' '"1"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '}}}}'

+ configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output Org1MSPanchors.tx

2023-03-16 04:56:37.029 UTC 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 04:56:37.048 UTC 0002 INFO [channelCmd] update -> Successfully submitted channel update

Anchor peer set for org 'Org1MSP' on channel 'mychannel'

Setting anchor peer for org2...

Using organization 2

Fetching channel config for channel mychannel

Using organization 2

Fetching the most recent configuration block for the channel

+ peer channel fetch config config_block.pb -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com -c mychannel --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem

2023-03-16 04:56:37.405 UTC 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 04:56:37.414 UTC 0002 INFO [cli.common] readBlock -> Received block: 1

2023-03-16 04:56:37.414 UTC 0003 INFO [channelCmd] fetch -> Retrieving last config block: 1

2023-03-16 04:56:37.417 UTC 0004 INFO [cli.common] readBlock -> Received block: 1

+ Decoding config block to JSON and isolating config to Org2MSPconfig.json

configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json

+ jq '.data.data[0].payload.data.config' config_block.json

Generating anchor peer update transaction for Org2 on channel mychannel

+ jq '.channel_group.groups.Application.groups.Org2MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org2.example.com","port": 9051}]},"version": "0"}}' Org2MSPconfig.json

+ configtxlator proto_encode --input Org2MSPconfig.json --type common.Config --output original_config.pb

+ configtxlator proto_encode --input Org2MSPmodified_config.json --type common.Config --output modified_config.pb

+ configtxlator compute_update --channel_id mychannel --original original_config.pb --updated modified_config.pb --output config_update.pb

+ configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json

+ jq .

++ cat config_update.json

+ echo '{"payload":{"header":{"channel_header":{"channel_id":"mychannel", "type":2}},"data":{"config_update":{' '"channel_id":' '"mychannel",' '"isolated_data":' '{},' '"read_set":' '{' '"groups":' '{' '"Application":' '{' '"groups":' '{' '"Org2MSP":' '{' '"groups":' '{},' '"mod_policy":' '"",' '"policies":' '{' '"Admins":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Endorsement":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Readers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Writers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '}' '},' '"values":' '{' '"MSP":' '{' '"mod_policy":' '"",' '"value":' null, '"version":' '"0"' '}' '},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '},' '"write_set":' '{' '"groups":' '{' '"Application":' '{' '"groups":' '{' '"Org2MSP":' '{' '"groups":' '{},' '"mod_policy":' '"Admins",' '"policies":' '{' '"Admins":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Endorsement":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Readers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '},' '"Writers":' '{' '"mod_policy":' '"",' '"policy":' null, '"version":' '"0"' '}' '},' '"values":' '{' '"AnchorPeers":' '{' '"mod_policy":' '"Admins",' '"value":' '{' '"anchor_peers":' '[' '{' '"host":' '"peer0.org2.example.com",' '"port":' 9051 '}' ']' '},' '"version":' '"0"' '},' '"MSP":' '{' '"mod_policy":' '"",' '"value":' null, '"version":' '"0"' '}' '},' '"version":' '"1"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '},' '"mod_policy":' '"",' '"policies":' '{},' '"values":' '{},' '"version":' '"0"' '}' '}}}}'

+ configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output Org2MSPanchors.tx

2023-03-16 04:56:37.721 UTC 0001 INFO [channelCmd] InitCmdFactory -> Endorser and orderer connections initialized

2023-03-16 04:56:37.736 UTC 0002 INFO [channelCmd] update -> Successfully submitted channel update

Anchor peer set for org 'Org2MSP' on channel 'mychannel'

Channel 'mychannel' joined

Docker Desktop

Step 6: Deploy, invoke the Chaincode (Smart Contract), and Interact with the Network

Subscribe to Aero Wong LLC

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe