From 0dfbfd5bcbea870ccb25ec21ef1feaaa2931e3c2 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 14:38:58 +0800 Subject: [PATCH 01/15] added part 5 --- new-member/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 new-member/README.md diff --git a/new-member/README.md b/new-member/README.md new file mode 100644 index 00000000..96b7a6cc --- /dev/null +++ b/new-member/README.md @@ -0,0 +1,19 @@ +# Part 5: Adding a new member to an existing channel + +Part 5 will help you to add a new member running in a different AWS Account to the Fabric network you created in [Part 1](../ngo-fabric/README.md). After adding the new member you will create a peer node for the member and join the peer +node to the channel created in [Part 1](../ngo-fabric/README.md). The new peer node will receive the blocks that exist +on the next channel and will build its own copy of the ledger. We will also configure the Fabric network so the new +member can take part in endorsing transactions. + +## Pre-requisites +There are multiple parts to the workshop. Before starting on Part 5 you should at least complete [Part 1](../ngo-fabric/README.md). You need an existing Fabric network in order to create a new member. + + + +## The workshop sections +The workshop instructions can be found in the README files in parts 1-4: + +* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the AWS Managed Blockchain Hyperledger Fabric network. +* [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. +* [Part 3:](../ngo-rest-api/README.md) Run the REST API. +* [Part 4:](../ngo-ui/README.md) Run the Application. From 98d5702f0afa63e2c30d320b0d2cf8402cda4c41 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 17:17:35 +0800 Subject: [PATCH 02/15] added part 5 --- new-member/README.md | 223 +++++++++++++++++- new-member/s3-handler.sh | 115 +++++++++ ngo-chaincode/README.md | 2 +- ngo-fabric/README.md | 20 +- ngo-fabric/templates/exports-template.sh | 1 - .../ngo-connection-profile-sample.yaml | 2 +- 6 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 new-member/s3-handler.sh diff --git a/new-member/README.md b/new-member/README.md index 96b7a6cc..212afb05 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -5,15 +5,232 @@ node to the channel created in [Part 1](../ngo-fabric/README.md). The new peer n on the next channel and will build its own copy of the ledger. We will also configure the Fabric network so the new member can take part in endorsing transactions. -## Pre-requisites -There are multiple parts to the workshop. Before starting on Part 5 you should at least complete [Part 1](../ngo-fabric/README.md). You need an existing Fabric network in order to create a new member. +Adding a new member to an existing Fabric network involves a number of steps. The new member is located in a different AWS account, and the steps therefore involve cooperation between administrators for the Fabric member in the existing account (let’s call this Account A), and the new account (let’s call this Account B). The steps look as follows: +1. Account A invites Account B to join the Fabric network +2. Account B creates a member in the Fabric network +3. Account B creates a peer node +4. Account B shares the public keys for its member with Account A +5. Account A updates the channel configuration with the MSP for Account B +6. Account A shares the genesis block for the channel with Account B +7. Account B starts its peer node and joins the channel +8. If Account B will take part in endorsing transaction proposals, Account B will install chaincode +## Pre-requisites - Account A, the network creator +There are multiple parts to the workshop. Before starting on Part 5, a network creator should haved completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. + +## Pre-requisites - Account B +We will use Cloud9 to provide a Linux terminal which has the AWS CLI already installed. + +1. Spin up a [Cloud9 IDE](https://us-east-1.console.aws.amazon.com/cloud9/home?region=us-east-1) from the AWS console. +In the Cloud9 console, click 'Create Environment'. Using 'us-east-1' for the region will be easier. +2. Provide a name for your environment, e.g. fabric-c9, and click **Next Step** +3. Select `Other instance type`, then select `t2-medium` and click **Next Step** +4. Click **Create environment**. It would typically take 30-60s to create your Cloud9 IDE +5. In the Cloud9 terminal, in the home directory, clone this repo: + +``` +cd ~ +git clone https://github.com/aws-samples/non-profit-blockchain.git +``` + +Download the model file for the new Amazon Managed Blockchain service. This is a temporary step +and will not be required once the `managedblockchain` service has been included in the latest CLI. + +``` +cd ~ +aws s3 cp s3://managedblockchain-beta/service-2.json . +aws configure add-model --service-model file://service-2.json --service-name managedblockchain +``` + +## Step 1: Account A invites Account B to join the Fabric network +In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain + +The admin user for Account A invites another AWS account to join the Fabric network. In the Amazon Managed Blockchain console, select your network and click the ‘Invite account’ button. Enter the 12-digit AWS account number. You should see a confirmation message indicating your invitation has been sent successfully. + +## Step 2: Account B creates a member in the Fabric network +In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain + +The admin user for Account B can view the invitation in the Amazon Managed Blockchain console. Clicking on the network name will show the details of the network that Account B has been invited to join. Click ‘Create member’ to create a member in the network, entering a unique member name and an administrator username and password for the member. Note down the admin username and password. + +## Step 3: Account B creates a peer node +In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain + +Once the Fabric network and member for Account B have an ACTIVE status, it’s time to create a Fabric peer node. Each member on a network creates their own peer nodes, so select the member you created above and click the link to create a peer node. Select an instance type, the amount of storage for that node, and create the peer node. + +## Step 4: Account B creates a Fabric client node +These steps are identical to those performed by Account A when the Fabric network was originally created. See Step 3 in [Part 1:](../ngo-fabric/README.md). The steps have been replicated below. + +In your Cloud9 terminal window. + +Create the Fabric client node, which will host the Fabric CLI. You will use the CLI to administer +the Fabric network. The Fabric client node will be created in its own VPC, with VPC endpoints +pointing to the Fabric network created by the network creator in [Part 1](../ngo-fabric/README.md). +CloudFormation will be used to create the Fabric client node, the VPC and the VPC endpoints. + +The CloudFormation template requires a number of parameter values. We'll make sure these +are available as export variables before running the script below. You can obtain these values +from the Amazon Managed Blockchain Console. + +In Cloud9: + +``` +export REGION=us-east-1 +export NETWORKID= +export NETWORKNAME= +``` + +Set the VPC endpoint. Make sure it has been populated and exported. If the `echo` statement below shows +that it's blank, check the details under your network in the Amazon Managed Blockchain Console: + +``` +export VPCENDPOINTSERVICENAME=$(aws managedblockchain get-network --region $REGION --network-id $NETWORKID --query 'Network.VpcEndpointServiceName' --output text) +echo $VPCENDPOINTSERVICENAME +``` + +If the VPC endpoint is populated with a value, go ahead and run this script. This will create the +CloudFormation stack. You will see an error saying `Keypair not found`. This is expected as the script +will check whether the keypair exists before creating it. I don't want to overwrite any existing +keypairs you have, so just ignore this error and let the script continue: + +``` +cd ~/non-profit-blockchain/ngo-fabric +./3-vpc-client-node.sh +``` + +Check the progress in the AWS CloudFormation console and wait until the stack is CREATE COMPLETE. +You will find some useful information in the Outputs tab of the CloudFormation stack once the stack +is complete. We will use this information in later steps. + +## Step 4 - Account B prepares the Fabric client node and enrolls an identity +On the Fabric client node. + +Prior to executing any commands in the Fabric client node, you will need to export ENV variables +that provide a context to Hyperledger Fabric. These variables will tell the client node which peer +node to interact with, which TLS certs to use, etc. + +From Cloud9, SSH into the Fabric client node. The key (i.e. the .PEM file) should be in your home directory. +The DNS of the Fabric client node EC2 instance can be found in the output of the CloudFormation stack you +created in Step 3 above. + +Answer 'yes' if prompted: `Are you sure you want to continue connecting (yes/no)` + +``` +cd ~ +ssh ec2-user@ -i ~/-keypair.pem +``` + +Clone the repo: + +``` +cd ~ +git clone https://github.com/aws-samples/non-profit-blockchain.git +``` + +Create the file that includes the ENV export values that define your Fabric network configuration. + +``` +cd ~/non-profit-blockchain/ngo-fabric +cp templates/exports-template.sh fabric-exports.sh +vi fabric-exports.sh +``` + +Update the export statements at the top of the file. The info you need can be found +in the Amazon Managed Blockchain Console, under your network. The member details you use, and +the admin username and password, are the ones you entered when creating your member. + +Source the file, so the exports are applied to your current session. If you exit the SSH +session and re-connect, you'll need to source the file again. + +``` +cd ~/non-profit-blockchain/ngo-fabric +source fabric-exports.sh +``` + +Sourcing the file will do two things: +* export the necessary ENV variables +* create another file which contains the export values you need to use when working with a Fabric peer node. +This can be found in the file: `~/peer-exports.sh`. You will see how to use this in a later step. + +Check the `source` worked: + +``` +$ echo $PEERSERVICEENDPOINT +nd-4MHB4EKFCRF7VBHXZE2ZU4F6GY.m-B7YYBFY4GREBZLPCO2SUS4GP3I.n-WDG36TTUD5HEJORZUPF4REKMBI.managedblockchain.us-east-1.amazonaws.com:30003 +``` + +Check the peer export file exists and that it contains a number of export keys with values: + +``` +cat ~/peer-exports.sh +``` + +If the file has values for all keys, source it: + +``` +source ~/peer-exports.sh +``` + +Enroll an admin identity with the Fabric CA (certificate authority). We will use this +identity to administer the Fabric network and perform tasks such as creating channels +and instantiating chaincode. + +``` +export PATH=$PATH:/home/ec2-user/go/src/github.com/hyperledger/fabric-ca/bin +cd ~ +fabric-ca-client enroll -u https://$ADMINUSER:$ADMINPWD@$CASERVICEENDPOINT --tls.certfiles /home/ec2-user/managedblockchain-tls-chain.pem -M /home/ec2-user/admin-msp +``` + +Some final copying of the certificates is necessary: + +``` +mkdir -p /home/ec2-user/admin-msp/admincerts +cp ~/admin-msp/signcerts/* ~/admin-msp/admincerts/ +cd ~/non-profit-blockchain/ngo-fabric +``` + +## Step 5: Account B shares the public keys for its member with Account A +I’m assuming this step is not required as Amazon Managed Blockchain will handle copying the certs after step 2. +Account B will also have a copy of the orderer PEM file, so no need to copy this + +## Step 6: Account A creates an MSP for the new Account A member +Account A stores the certificates provided by Account B on its Fabric client node + +## Step 7: Account A updates the channel configuration with the MSP for Account B +This step generates a new channel configuration block that includes the new member belonging to Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. + +A new channel configuration block is created by fetching the latest configuration block from the channel, generating a new channel configuration, then comparing the old and new configurations to generate a 'diff'. + +## Step 8: Endorsing peers must sign the new channel configuration +The 'diff' must be signed by the existing network members, i.e. the network members must endorse the changes to the channel configuration and approve the new member joining the channel. A channel configuration update is really just another transaction in Fabric, known as a 'configuration transaction', and as such it must be endorsed by network members in accordance with the modification policy for the channel. The default modification policy for the channel Application group is MAJORITY, which means a majority of members need to sign the channel configuration update. + +To allow admins belonging to different members to sign the channel configuration you will need to pass the channel configuration ‘diff’ file to each member in the network, one-by-one, and have them sign the channel configuration. Each member signature must be applied in turn so that we end up with a package that has the signatures of all endorsing members. Alternatively, you could send the channel config to all members simultaneously and wait to receive signed responses, but then you would have to extract the signatures from the individual responses and create a single package which contains the configuration update plus all the required signatures. + +At this point you may only have two members: the member owned by the network creator, and the new member. In this case, only the member owned by the network creator needs to sign the package. However, I have made this step explicit because you will need to +include this step as more members join the network. + +Once we have a signed channel configuration we can apply it to the channel. + +## Step 9: Account A updates the channel with the new configuration +In this step we update the channel with the new channel configuration. Since the new channel configuration now includes details +of the new organisation, this will allow the new organisation to join the channel. + +## Step 10: Account A shares the genesis block for the channel with Account B +Before the peer node in Account B joins the channel, it must be able to connect to the Ordering Service managed by Amazon Managed Blockchain. It obtains the Ordering Service endpoint from the channel genesis block. The file mychannel.block ('mychannel' refers to the channel name and may differ if you have changed the channel name) would have been created when you first created the channel. Make sure the mychannel.block file is available to the peer node in Account B. + +## Step 11: Account B starts its peer node and joins the channel +The next step is to join the peer node to the channel. After the peer successfully joins the channel it will start receiving blocks of transactions and build its own copy of the ledger, creating the blockchain and populating the world state key-value store. + +## Step 12: Account B installs chaincode +After install the chaincode, the peer node in Account B will be able to run queries and endorse transactions. + +## Step 13: Account B queries chaincode and invokes transactions +To check that the new member owned by Account B is a full member of the Fabric network, the peer node will execute queries against its own ledger and invoke transactions on the network. Since the new member is included in the endorsement policy we expect to this transactions endorsed by the new member. ## The workshop sections The workshop instructions can be found in the README files in parts 1-4: -* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the AWS Managed Blockchain Hyperledger Fabric network. +* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the Amazon Managed Blockchain Hyperledger Fabric network. * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh new file mode 100644 index 00000000..babf3975 --- /dev/null +++ b/new-member/s3-handler.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +set +e + +region=us-east-1 +memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +S3BucketNameCreator=${memberID}-creator +S3BucketNameNewMember=${memberID}-newmember + +# copy the certificates for the new Fabric member to S3 +function copyCertsToS3 { + echo "Copying the certs for the new org to S3" + if [[ $(aws configure list) && $? -eq 0 ]]; then + aws s3api put-object --bucket $S3BucketNameNewMember --key ${memberID}/admincerts --body /home/ec2-user/admin-msp/admincerts/cert.pem + aws s3api put-object --bucket $S3BucketNameNewMember --key ${memberID}/cacerts --body /home/ec2-user/admin-msp/cacerts/*.pem + aws s3api put-object-acl --bucket $S3BucketNameNewMember --key ${memberID}/admincerts --acl public-read + aws s3api put-object-acl --bucket $S3BucketNameNewMember --key ${memberID}/cacerts --acl public-read + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Copying the certs for the new org to S3 complete" +} + +# copy the certificates for the new Fabric member from S3 to the Fabric creator network +function copyCertsFromS3 { + echo "Copying the certs from S3" + if [[ $(aws configure list) && $? -eq 0 ]]; then + aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/admincerts /home/ec2-user/${memberID}-msp/admincerts/cert.pem + aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/cacerts /home/ec2-user/${memberID}-msp/admincerts/cacert.pem + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Copying the certs from S3 complete" +} + +# copy the Channel Genesis block from the Fabric creator network to S3 +function copyChannelGenesisToS3 { + echo "Copying the Channel Genesis block to S3" + if [[ $(aws configure list) && $? -eq 0 ]]; then + aws s3api put-object --bucket $S3BucketNameCreator --key org0/mychannel.block --body ${DATA}/mychannel.block + aws s3api put-object-acl --bucket $S3BucketNameCreator --key org0/mychannel.block --grant-read uri=http://acs.amazonaws.com/groups/global/AllUsers + aws s3api put-object-acl --bucket $S3BucketNameCreator --key org0/mychannel.block --acl public-read + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Copying the Channel Genesis block to S3 complete" +} + +# copy the Channel Genesis block from S3 to the new Fabric member +function copyChannelGenesisFromS3 { + echo "Copying the Channel Genesis block from S3" + if [[ $(aws configure list) && $? -eq 0 ]]; then + sudo chown ec2-user ${DATA}/mychannel.block + aws s3api get-object --bucket $S3BucketNameCreator --key org0/mychannel.block ${DATA}/mychannel.block + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Copying the Channel Genesis block from S3 complete" +} + +# create S3 bucket to copy files from the Fabric network creator. Bucket will be read-only to other members +function createS3BucketForCreator { + #create the s3 bucket + echo -e "creating s3 bucket for network creator: $S3BucketNameCreator" + #quick way of determining whether the AWS CLI is installed and a default profile exists + if [[ $(aws configure list) && $? -eq 0 ]]; then + if [[ "$region" == "us-east-1" ]]; then + aws s3api create-bucket --bucket $S3BucketNameCreator --region $region + else + aws s3api create-bucket --bucket $S3BucketNameCreator --region $region --create-bucket-configuration LocationConstraint=$region + fi + aws s3api put-bucket-acl --bucket $S3BucketNameCreator --grant-read uri=http://acs.amazonaws.com/groups/global/AllUsers + aws s3api put-bucket-acl --bucket $S3BucketNameCreator --acl public-read + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Creating the S3 bucket complete" +} + +# create S3 bucket to copy files from the new member. Bucket will be read-only to other members +function createS3BucketForNewMember { + #create the s3 bucket + echo -e "creating s3 bucket for new member $NEW_ORG: $S3BucketNameNewMember" + #quick way of determining whether the AWS CLI is installed and a default profile exists + if [[ $(aws configure list) && $? -eq 0 ]]; then + if [[ "$region" == "us-east-1" ]]; then + aws s3api create-bucket --bucket $S3BucketNameNewMember --region $region + else + aws s3api create-bucket --bucket $S3BucketNameNewMember --region $region --create-bucket-configuration LocationConstraint=$region + fi + aws s3api put-bucket-acl --bucket $S3BucketNameNewMember --grant-read uri=http://acs.amazonaws.com/groups/global/AllUsers + aws s3api put-bucket-acl --bucket $S3BucketNameNewMember --acl public-read + else + echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" + fi + echo "Creating the S3 bucket complete" +} + +# This is a little hack I found here: https://stackoverflow.com/questions/8818119/how-can-i-run-a-function-from-a-script-in-command-line +# that allows me to call this bash script and invoke a specific function from the command line +"$@" + diff --git a/ngo-chaincode/README.md b/ngo-chaincode/README.md index 6f388e34..845b797d 100644 --- a/ngo-chaincode/README.md +++ b/ngo-chaincode/README.md @@ -165,7 +165,7 @@ docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt ## Move on to Part 3 The workshop instructions can be found in the README files in parts 1-4: -* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the AWS Managed Blockchain Hyperledger Fabric network. +* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the Amazon Managed Blockchain Hyperledger Fabric network. * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. diff --git a/ngo-fabric/README.md b/ngo-fabric/README.md index 6446c81c..ec4d8731 100644 --- a/ngo-fabric/README.md +++ b/ngo-fabric/README.md @@ -1,10 +1,10 @@ # Part1: Setup a Fabric network -This section will create an AWS Managed Blockchain Fabric network. A combination of the AWS Console and the AWS CLI +This section will create an Amazon Managed Blockchain Fabric network. A combination of the AWS Console and the AWS CLI will be used. The process to create the network is as follows: * Provision a Cloud9 instance. We will use the Linux terminal that Cloud9 provides -* Use the AWS Managed Blockchain console to create a Fabric network and provision a peer node +* Use the Amazon Managed Blockchain Console to create a Fabric network and provision a peer node * From Cloud9, run a CloudFormation template to provision a VPC and a Fabric client node. You will use the Fabric client node to administer the Fabric network * From the Fabric client node, create a Fabric channel, install & instantiate chaincode, and @@ -25,7 +25,7 @@ cd ~ git clone https://github.com/aws-samples/non-profit-blockchain.git ``` -Download the model file for the new AWS Managed Blockchain service. This is a temporary step +Download the model file for the new Amazon Managed Blockchain service. This is a temporary step and will not be required once the `managedblockchain` service has been included in the latest CLI. ``` @@ -35,7 +35,7 @@ aws configure add-model --service-model file://service-2.json --service-name man ``` ## Step 1 - Create the Fabric network -In the AWS Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain +In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain Make sure you are in the correct AWS region (i.e. us-east-1, also known as N. Virginia) and follow the steps below: @@ -51,7 +51,7 @@ Before continuing, check to see that your Fabric network has been created and is wait for it to complete. Otherwise the steps below may fail. ## Step 2 - Create the Fabric Peer -In the AWS Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain +In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain 1. In the new network you have created, click on the member in the Members section. 2. Click `Create peer node` @@ -74,12 +74,12 @@ In Cloud9: ``` export REGION=us-east-1 -export NETWORKID= +export NETWORKID= export NETWORKNAME= ``` Set the VPC endpoint. Make sure it has been populated and exported. If the `echo` statement below shows -that it's blank, check the details under your network in the AWS Managed Blockchain Console: +that it's blank, check the details under your network in the Amazon Managed Blockchain Console: ``` export VPCENDPOINTSERVICENAME=$(aws managedblockchain get-network --region $REGION --network-id $NETWORKID --query 'Network.VpcEndpointServiceName' --output text) @@ -135,7 +135,7 @@ vi fabric-exports.sh Update the export statements at the top of the file. The info you need either matches what you entered when creating the Fabric network in [Part 1](../ngo-fabric/README.md), or can be found -in the AWS Managed Blockchain Console, under your network. +in the Amazon Managed Blockchain Console, under your network. Source the file, so the exports are applied to your current session. If you exit the SSH session and re-connect, you'll need to source the file again. @@ -191,7 +191,7 @@ cd ~/non-profit-blockchain/ngo-fabric On the Fabric client node. Update the configtx channel configuration. The Name and ID fields should be updated with the member ID. -You can obtain the member ID from the AWS Managed Blockchain Console, or from the ENV variables +You can obtain the member ID from the Amazon Managed Blockchain Console, or from the ENV variables exported to your current session. ``` @@ -414,7 +414,7 @@ You should see: ## Move on to Part 2 The workshop instructions can be found in the README files in parts 1-4: -* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the AWS Managed Blockchain Hyperledger Fabric network. +* [Part 1:](../ngo-fabric/README.md) Start the workshop by building the Amazon Managed Blockchain Hyperledger Fabric network. * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. diff --git a/ngo-fabric/templates/exports-template.sh b/ngo-fabric/templates/exports-template.sh index 3ff8a5b0..c9ea077e 100755 --- a/ngo-fabric/templates/exports-template.sh +++ b/ngo-fabric/templates/exports-template.sh @@ -47,7 +47,6 @@ export PEEREVENTENDPOINT=$eventEndPoint echo Useful information used in Cloud9 echo REGION: $REGION -echo ENDPOINT: $ENDPOINT echo NETWORKNAME: $NETWORKNAME echo NETWORKVERSION: $NETWORKVERSION echo ADMINUSER: $ADMINUSER diff --git a/ngo-rest-api/connection-profile/ngo-connection-profile-sample.yaml b/ngo-rest-api/connection-profile/ngo-connection-profile-sample.yaml index d744edd9..e8cbadaf 100644 --- a/ngo-rest-api/connection-profile/ngo-connection-profile-sample.yaml +++ b/ngo-rest-api/connection-profile/ngo-connection-profile-sample.yaml @@ -11,7 +11,7 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. -# Sample connection profile for an AWS Managed Blockchain Fabric network +# Sample connection profile for an Amazon Managed Blockchain Fabric network name: "ngo" x-type: "hlfv1" From 69f3c7c5a9576c80a38fffc06d4269b66735ae8d Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 17:30:03 +0800 Subject: [PATCH 03/15] added part 5 --- new-member/README.md | 20 ++++++++++++++++++-- new-member/s3-handler.sh | 3 +++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 212afb05..7c54ad24 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -190,8 +190,24 @@ cd ~/non-profit-blockchain/ngo-fabric ``` ## Step 5: Account B shares the public keys for its member with Account A -I’m assuming this step is not required as Amazon Managed Blockchain will handle copying the certs after step 2. -Account B will also have a copy of the orderer PEM file, so no need to copy this +Account B needs to share two certificates with Account A: + +Information will be shared via S3. Account B will copy the certs to S3, and Account A will fetch them from S3. + +Update the region and member ID in the following script: + +``` +cd ~/non-profit-blockchain +vi new-member/s3-handler.sh +``` + +Copy the Account B public keys to S3: + +```bash +cd ~/non-profit-blockchain +./new-member/s3-handler.sh createS3BucketForNewMember +./new-member/s3-handler.sh copyCertsToS3 +``` ## Step 6: Account A creates an MSP for the new Account A member Account A stores the certificates provided by Account B on its Fabric client node diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index babf3975..e9148a0f 100644 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -20,6 +20,9 @@ memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM S3BucketNameCreator=${memberID}-creator S3BucketNameNewMember=${memberID}-newmember +# convert memberID to lowercase. S3 buckets must be lower case +memberID=$(echo "$memberID" | tr '[:upper:]' '[:lower:]') + # copy the certificates for the new Fabric member to S3 function copyCertsToS3 { echo "Copying the certs for the new org to S3" From c35036d27d583ccc99a1b59505a297f91a6728b9 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 17:33:11 +0800 Subject: [PATCH 04/15] added part 5 --- new-member/s3-handler.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 new-member/s3-handler.sh diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh old mode 100644 new mode 100755 index e9148a0f..44065955 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -17,11 +17,11 @@ set +e region=us-east-1 memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM -S3BucketNameCreator=${memberID}-creator -S3BucketNameNewMember=${memberID}-newmember # convert memberID to lowercase. S3 buckets must be lower case memberID=$(echo "$memberID" | tr '[:upper:]' '[:lower:]') +S3BucketNameCreator=${memberID}-creator +S3BucketNameNewMember=${memberID}-newmember # copy the certificates for the new Fabric member to S3 function copyCertsToS3 { From a99f525bb826d29174eaef43fda9eb1ac870b786 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 17:42:32 +0800 Subject: [PATCH 05/15] added part 5 --- new-member/README.md | 27 +++++++++++++++++++++++++-- new-member/s3-handler.sh | 4 +++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 7c54ad24..7be5a86f 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -190,11 +190,16 @@ cd ~/non-profit-blockchain/ngo-fabric ``` ## Step 5: Account B shares the public keys for its member with Account A +On the Fabric client node in Account B. + Account B needs to share two certificates with Account A: +* the admin cert, stored in /home/ec2-user/admin-msp/admincerts +* the root CA cert, store in /home/ec2-user/admin-msp/cacerts + Information will be shared via S3. Account B will copy the certs to S3, and Account A will fetch them from S3. -Update the region and member ID in the following script: +Update the region and member ID in the following script. The member ID is the ID of the new member in Account B: ``` cd ~/non-profit-blockchain @@ -210,7 +215,25 @@ cd ~/non-profit-blockchain ``` ## Step 6: Account A creates an MSP for the new Account A member -Account A stores the certificates provided by Account B on its Fabric client node +On the Fabric client node in Account A. + +Account A stores the certificates provided by Account B on its Fabric client node. + +Update the region and member ID in the following script. The member ID is the ID of the new member in Account B, so this +file should look identical to the one created in the previous step: + +``` +cd ~/non-profit-blockchain +vi new-member/s3-handler.sh +``` + +Copy the Account B public keys from S3 to the MSP directory on the Fabric client node in Account A: + +```bash +cd ~/non-profit-blockchain +./new-member/s3-handler.sh copyCertsFromS3 +``` + ## Step 7: Account A updates the channel configuration with the MSP for Account B This step generates a new channel configuration block that includes the new member belonging to Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index 44065955..59780b1c 100755 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -41,8 +41,10 @@ function copyCertsToS3 { function copyCertsFromS3 { echo "Copying the certs from S3" if [[ $(aws configure list) && $? -eq 0 ]]; then + mkdir -p /home/ec2-user/${memberID}-msp/admincerts + mkdir -p /home/ec2-user/${memberID}-msp/cacerts aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/admincerts /home/ec2-user/${memberID}-msp/admincerts/cert.pem - aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/cacerts /home/ec2-user/${memberID}-msp/admincerts/cacert.pem + aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/cacerts /home/ec2-user/${memberID}-msp/cacerts/cacert.pem else echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" fi From 791c4032c252ca507d9423120ec0e6e7f23ed2dd Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 14 Dec 2018 18:28:14 +0800 Subject: [PATCH 06/15] added part 5 --- new-member/README.md | 253 ++++++++++++++++++++++++++++- new-member/create-config-update.sh | 77 +++++++++ new-member/s3-handler.sh | 1 + 3 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 new-member/create-config-update.sh diff --git a/new-member/README.md b/new-member/README.md index 7be5a86f..a72cbea8 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -19,6 +19,45 @@ Adding a new member to an existing Fabric network involves a number of steps. Th ## Pre-requisites - Account A, the network creator There are multiple parts to the workshop. Before starting on Part 5, a network creator should haved completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. +From Cloud9, SSH into the Fabric client node. The key (i.e. the .PEM file) should be in your home directory. +The DNS of the Fabric client node EC2 instance can be found in the output of the CloudFormation stack you +created in [Part 1](../ngo-fabric/README.md) + +``` +ssh ec2-user@ -i ~/-keypair.pem +``` + +You should have already cloned this repo in [Part 1](../ngo-fabric/README.md) + +``` +cd ~ +git clone https://github.com/aws-samples/non-profit-blockchain.git +``` + +You will need to set the context before carrying out any Fabric CLI commands. We do this +using the export files that were generated for us in [Part 1](../ngo-fabric/README.md) + +Source the file, so the exports are applied to your current session. If you exit the SSH +session and re-connect, you'll need to source the file again. The `source` command below +will print out the values of the key ENV variables. Make sure they are all populated. If +they are not, follow Step 4 in [Part 1](../ngo-fabric/README.md) to repopulate them: + +``` +cd ~/non-profit-blockchain/ngo-fabric +source fabric-exports.sh +``` + +Check the peer export file exists and that it contains a number of export keys with values: + +``` +cat ~/peer-exports.sh +``` +If the file has values for all keys, source it: + +``` +source ~/peer-exports.sh +``` + ## Pre-requisites - Account B We will use Cloud9 to provide a Linux terminal which has the AWS CLI already installed. @@ -234,12 +273,224 @@ cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsFromS3 ``` +## Step 7: Account A updates the configtx.yaml configuration with the MSP for Account B +On the Fabric client node in Account A. + +The configtx.yaml file contains details of the organisations in a Fabric network as well as channel configuration profiles that can be used when creating new channels. The channel creator originally created this file just before creating the channel. The channel creator now needs to add the new member to this file. + +You will find the file to be edited in the home directory of the Fabric client node: + +``` +vi ~/configtx.yaml +``` + +You will make two changes to the file: + +1. Add Org2 (or Org3, Org4, etc. - just copy &Org2 under Organizations from the template below), replacing `Member2ID` with the ID of your member as copied directly from the Amazon Managed Blockchain console. For the MSPDir, `Member2ID` must be replaced by a lowercase member ID. Instead of typing this, you can copy it from your Fabric client node. Enter: `ls ~` and find the directory with your lower case member ID. The end result of the MSPDir in configtx.yaml should look as follows: `MSPDir: /opt/home/m-trd4xpjborem7bdmbv6wlg3nkm-msp` +2. Add the TwoOrgChannel under Organizations, under Profiles, at the end of the file. + +Save the file. + +Important + +This file is sensitive and must be indented properly. Artifacts from pasting can cause the file to fail with marshalling errors. We recommend using emacs to edit it. You can also use VI, but before making any changes in VI, enter `:set paste`, press i to enter insert mode, paste the contents, press escape, and then enter `:set nopaste` before saving. + +### Template for configtx.yaml + +``` +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + - &Org1 + # member id defines the organization + Name: Member1ID + # ID to load the MSP definition as + ID: Member1ID + #msp dir of org1 in the docker container + MSPDir: /opt/home/admin-msp + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + AnchorPeers: + - Host: + Port: + - &Org2 + Name: Member2ID + ID: Member2ID + MSPDir: /opt/home/Member2ID-msp + AnchorPeers: + - Host: + Port: + +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + OneOrgChannel: + Consortium: AWSSystemConsortium + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + TwoOrgChannel: + Consortium: AWSSystemConsortium + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 +``` + +### Working example of a complete configtx.yaml + +``` +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + - &Org1 + Name: m-TEW3EJGTPBBW7BMGXYOIXV5364 + + # ID to load the MSP definition as + ID: m-TEW3EJGTPBBW7BMGXYOIXV5364 + + MSPDir: /opt/home/admin-msp + + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: + Port: + + - &Org2 + Name: m-TRD4XPJBOREM7BDMBV6WLG3NKM + ID: m-TRD4XPJBOREM7BDMBV6WLG3NKM + MSPDir: /opt/home/m-trd4xpjborem7bdmbv6wlg3nkm-msp + AnchorPeers: + - Host: + Port: + +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + OneOrgChannel: + Consortium: AWSSystemConsortium + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + TwoOrgChannel: + Consortium: AWSSystemConsortium + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 +``` ## Step 7: Account A updates the channel configuration with the MSP for Account B -This step generates a new channel configuration block that includes the new member belonging to Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. +On the Fabric client node in Account A. + +This step generates a new channel configuration block that includes the new member owned by Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. A new channel configuration block is created by fetching the latest configuration block from the channel, generating a new channel configuration, then comparing the old and new configurations to generate a 'diff'. +Generating a new channel configuration involves a number of steps, which we will work through below. + +### Generate configtx channel configuration +Generate the configtx channel configuration by executing the following script: + +``` +docker exec cli configtxgen -outputCreateChannelTx /opt/home/$CHANNEL.pb -profile TwoOrgChannel -channelID $CHANNEL --configPath /opt/home/ +``` + +You should see: + +``` +2018-11-26 21:41:22.885 UTC [common/tools/configtxgen] main -> INFO 001 Loading configuration +2018-11-26 21:41:22.887 UTC [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 002 Generating new channel configtx +2018-11-26 21:41:22.887 UTC [common/tools/configtxgen/encoder] NewApplicationGroup -> WARN 003 Default policy emission is deprecated, please include policy specificiations for the application group in configtx.yaml +2018-11-26 21:41:22.887 UTC [common/tools/configtxgen/encoder] NewApplicationOrgGroup -> WARN 004 Default policy emission is deprecated, please include policy specificiations for the application org group m-BHX24CQGP5CUNFS3YZTO2MPSRI in configtx.yaml +2018-11-26 21:41:22.888 UTC [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 005 Writing new channel tx +``` + +Check that the channel configuration has been generated: + +``` +ls -lt ~/$CHANNEL.pb +``` + +### Fetch the latest config block from the channel +The genesis block for the channel exists here: + +``` +ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer +``` + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer channel fetch config /opt/home/fabric-samples/chaincode/hyperledger/fabric/peer/$CHANNEL.config.block \ + -c $CHANNEL -o $ORDERER --cafile /opt/home/managedblockchain-tls-chain.pem --tls +``` + +Check that the latest config block file now exists: + +``` +ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer +``` + + ## Step 8: Endorsing peers must sign the new channel configuration The 'diff' must be signed by the existing network members, i.e. the network members must endorse the changes to the channel configuration and approve the new member joining the channel. A channel configuration update is really just another transaction in Fabric, known as a 'configuration transaction', and as such it must be endorsed by network members in accordance with the modification policy for the channel. The default modification policy for the channel Application group is MAJORITY, which means a majority of members need to sign the channel configuration update. diff --git a/new-member/create-config-update.sh b/new-member/create-config-update.sh new file mode 100644 index 00000000..2be8b2ce --- /dev/null +++ b/new-member/create-config-update.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +function createConfigUpdate { + log "Creating config update payload for the new member '$NEW_ORG'" + + # Start the configtxlator + configtxlator start & + configtxlator_pid=$! + log "configtxlator_pid:$configtxlator_pid" + log "Sleeping 5 seconds for configtxlator to start..." + sleep 5 + + pushd /tmp + + #make a copy of the .json files below + jsonbkdir=/$DATA/addorg-${NEW_ORG}-`date +%Y%m%d-%H%M` + mkdir $jsonbkdir + + CTLURL=http://127.0.0.1:7059 + # Convert the config block protobuf to JSON + curl -X POST --data-binary @$CONFIG_BLOCK_FILE $CTLURL/protolator/decode/common.Block > ${NEW_ORG}_config_block.json + # Extract the config from the config block + jq .data.data[0].payload.data.config ${NEW_ORG}_config_block.json > ${NEW_ORG}_config.json + sudo cp ${NEW_ORG}_config_block.json $jsonbkdir + sudo cp ${NEW_ORG}_config.json $jsonbkdir + + isOrgInChannelConfig ${NEW_ORG}_config.json + if [ $? -eq 0 ]; then + log "Org '$NEW_ORG' already exists in the channel config. Config will not be updated. Exiting createConfigUpdate" + return 1 + fi + + # Append the new org configuration information + jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"'$NEW_ORG'":.[1]}}}}}' ${NEW_ORG}_config.json ${NEW_ORG}.json > ${NEW_ORG}_updated_config.json + # copy the block config to the /data directory in case we need to update it with another config change later + cp /tmp/${NEW_ORG}_updated_config.json $jsonbkdir + + # Create the config diff protobuf + curl -X POST --data-binary @${NEW_ORG}_config.json $CTLURL/protolator/encode/common.Config > ${NEW_ORG}_config.pb + curl -X POST --data-binary @${NEW_ORG}_updated_config.json $CTLURL/protolator/encode/common.Config > ${NEW_ORG}_updated_config.pb + curl -X POST -F original=@${NEW_ORG}_config.pb -F updated=@${NEW_ORG}_updated_config.pb $CTLURL/configtxlator/compute/update-from-configs -F channel=$CHANNEL_NAME > ${NEW_ORG}_config_update.pb + + # Convert the config diff protobuf to JSON + curl -X POST --data-binary @${NEW_ORG}_config_update.pb $CTLURL/protolator/decode/common.ConfigUpdate > ${NEW_ORG}_config_update.json + cp /tmp/${NEW_ORG}_config_update.json $jsonbkdir + + # Create envelope protobuf container config diff to be used in the "peer channel update" command to update the channel configuration block + echo '{"payload":{"header":{"channel_header":{"channel_id":"'"${CHANNEL_NAME}"'", "type":2}},"data":{"config_update":'$(cat ${NEW_ORG}_config_update.json)'}}}' > ${NEW_ORG}_config_update_as_envelope.json + curl -X POST --data-binary @${NEW_ORG}_config_update_as_envelope.json $CTLURL/protolator/encode/common.Envelope > /tmp/${NEW_ORG}_config_update_as_envelope.pb + # copy to the /data directory so the file can be signed by other admins + cp /tmp/${NEW_ORG}_config_update_as_envelope.pb /$DATA + cp /tmp/${NEW_ORG}_config_update_as_envelope.pb $jsonbkdir + cp /tmp/${NEW_ORG}_config_update_as_envelope.json $jsonbkdir + ls -lt $jsonbkdir + + # Stop configtxlator + kill $configtxlator_pid + log "Created config update payload for the new organization '$NEW_ORG', in file /${DATA}/${NEW_ORG}_config_update_as_envelope.pb" + + popd + return 0 +} + +createConfigUpdate diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index 59780b1c..683b200d 100755 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -45,6 +45,7 @@ function copyCertsFromS3 { mkdir -p /home/ec2-user/${memberID}-msp/cacerts aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/admincerts /home/ec2-user/${memberID}-msp/admincerts/cert.pem aws s3api get-object --bucket $S3BucketNameNewMember --key ${memberID}/cacerts /home/ec2-user/${memberID}-msp/cacerts/cacert.pem + ls -lR /home/ec2-user/${memberID}-msp/ else echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" fi From e24e99be00454af34c3b43a72c1843b1445adbf8 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Thu, 20 Dec 2018 15:12:54 +0800 Subject: [PATCH 07/15] added part 5 --- new-member/README.md | 7 ++-- new-member/create-config-update.sh | 52 +++++++++++++----------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index a72cbea8..505c6c5a 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -225,7 +225,6 @@ Some final copying of the certificates is necessary: ``` mkdir -p /home/ec2-user/admin-msp/admincerts cp ~/admin-msp/signcerts/* ~/admin-msp/admincerts/ -cd ~/non-profit-blockchain/ngo-fabric ``` ## Step 5: Account B shares the public keys for its member with Account A @@ -284,7 +283,7 @@ You will find the file to be edited in the home directory of the Fabric client n vi ~/configtx.yaml ``` -You will make two changes to the file: +You will make two changes to the file. A working example of the updated configtx.yaml file can be found below the template example below: 1. Add Org2 (or Org3, Org4, etc. - just copy &Org2 under Organizations from the template below), replacing `Member2ID` with the ID of your member as copied directly from the Amazon Managed Blockchain console. For the MSPDir, `Member2ID` must be replaced by a lowercase member ID. Instead of typing this, you can copy it from your Fabric client node. Enter: `ls ~` and find the directory with your lower case member ID. The end result of the MSPDir in configtx.yaml should look as follows: `MSPDir: /opt/home/m-trd4xpjborem7bdmbv6wlg3nkm-msp` 2. Add the TwoOrgChannel under Organizations, under Profiles, at the end of the file. @@ -451,7 +450,7 @@ Generating a new channel configuration involves a number of steps, which we will Generate the configtx channel configuration by executing the following script: ``` -docker exec cli configtxgen -outputCreateChannelTx /opt/home/$CHANNEL.pb -profile TwoOrgChannel -channelID $CHANNEL --configPath /opt/home/ +docker exec cli configtxgen -outputCreateChannelTx /opt/home/$CHANNEL-two-org.pb -profile TwoOrgChannel -channelID $CHANNEL --configPath /opt/home/ ``` You should see: @@ -467,7 +466,7 @@ You should see: Check that the channel configuration has been generated: ``` -ls -lt ~/$CHANNEL.pb +ls -lt $CHANNEL-two-org.pb ``` ### Fetch the latest config block from the channel diff --git a/new-member/create-config-update.sh b/new-member/create-config-update.sh index 2be8b2ce..bf175ef4 100644 --- a/new-member/create-config-update.sh +++ b/new-member/create-config-update.sh @@ -13,9 +13,13 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. +region=us-east-1 +memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +blockDir=/home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer + function createConfigUpdate { - log "Creating config update payload for the new member '$NEW_ORG'" - + log "Creating config update payload for the new member '$memberID'" + # Start the configtxlator configtxlator start & configtxlator_pid=$! @@ -25,50 +29,38 @@ function createConfigUpdate { pushd /tmp - #make a copy of the .json files below - jsonbkdir=/$DATA/addorg-${NEW_ORG}-`date +%Y%m%d-%H%M` - mkdir $jsonbkdir - CTLURL=http://127.0.0.1:7059 # Convert the config block protobuf to JSON - curl -X POST --data-binary @$CONFIG_BLOCK_FILE $CTLURL/protolator/decode/common.Block > ${NEW_ORG}_config_block.json + curl -X POST --data-binary @$blockDir/$CHANNEL.config.block $CTLURL/protolator/decode/common.Block > ${memberID}_config_block.json # Extract the config from the config block - jq .data.data[0].payload.data.config ${NEW_ORG}_config_block.json > ${NEW_ORG}_config.json - sudo cp ${NEW_ORG}_config_block.json $jsonbkdir - sudo cp ${NEW_ORG}_config.json $jsonbkdir - - isOrgInChannelConfig ${NEW_ORG}_config.json + jq .data.data[0].payload.data.config ${memberID}_config_block.json > ${memberID}_config.json + + isOrgInChannelConfig ${memberID}_config.json if [ $? -eq 0 ]; then - log "Org '$NEW_ORG' already exists in the channel config. Config will not be updated. Exiting createConfigUpdate" + log "Org '$memberID' already exists in the channel config. Config will not be updated. Exiting createConfigUpdate" return 1 fi # Append the new org configuration information - jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"'$NEW_ORG'":.[1]}}}}}' ${NEW_ORG}_config.json ${NEW_ORG}.json > ${NEW_ORG}_updated_config.json - # copy the block config to the /data directory in case we need to update it with another config change later - cp /tmp/${NEW_ORG}_updated_config.json $jsonbkdir - + jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"'$memberID'":.[1]}}}}}' ${memberID}_config.json ${memberID}.json > ${memberID}_updated_config.json + # Create the config diff protobuf - curl -X POST --data-binary @${NEW_ORG}_config.json $CTLURL/protolator/encode/common.Config > ${NEW_ORG}_config.pb - curl -X POST --data-binary @${NEW_ORG}_updated_config.json $CTLURL/protolator/encode/common.Config > ${NEW_ORG}_updated_config.pb - curl -X POST -F original=@${NEW_ORG}_config.pb -F updated=@${NEW_ORG}_updated_config.pb $CTLURL/configtxlator/compute/update-from-configs -F channel=$CHANNEL_NAME > ${NEW_ORG}_config_update.pb + curl -X POST --data-binary @${memberID}_config.json $CTLURL/protolator/encode/common.Config > ${memberID}_config.pb + curl -X POST --data-binary @${memberID}_updated_config.json $CTLURL/protolator/encode/common.Config > ${memberID}_updated_config.pb + curl -X POST -F original=@${memberID}_config.pb -F updated=@${memberID}_updated_config.pb $CTLURL/configtxlator/compute/update-from-configs -F channel=$CHANNEL_NAME > ${memberID}_config_update.pb # Convert the config diff protobuf to JSON - curl -X POST --data-binary @${NEW_ORG}_config_update.pb $CTLURL/protolator/decode/common.ConfigUpdate > ${NEW_ORG}_config_update.json - cp /tmp/${NEW_ORG}_config_update.json $jsonbkdir + curl -X POST --data-binary @${memberID}_config_update.pb $CTLURL/protolator/decode/common.ConfigUpdate > ${memberID}_config_update.json # Create envelope protobuf container config diff to be used in the "peer channel update" command to update the channel configuration block - echo '{"payload":{"header":{"channel_header":{"channel_id":"'"${CHANNEL_NAME}"'", "type":2}},"data":{"config_update":'$(cat ${NEW_ORG}_config_update.json)'}}}' > ${NEW_ORG}_config_update_as_envelope.json - curl -X POST --data-binary @${NEW_ORG}_config_update_as_envelope.json $CTLURL/protolator/encode/common.Envelope > /tmp/${NEW_ORG}_config_update_as_envelope.pb + echo '{"payload":{"header":{"channel_header":{"channel_id":"'"${CHANNEL}"'", "type":2}},"data":{"config_update":'$(cat ${memberID}_config_update.json)'}}}' > ${memberID}_config_update_as_envelope.json + curl -X POST --data-binary @${memberID}_config_update_as_envelope.json $CTLURL/protolator/encode/common.Envelope > /tmp/${memberID}_config_update_as_envelope.pb # copy to the /data directory so the file can be signed by other admins - cp /tmp/${NEW_ORG}_config_update_as_envelope.pb /$DATA - cp /tmp/${NEW_ORG}_config_update_as_envelope.pb $jsonbkdir - cp /tmp/${NEW_ORG}_config_update_as_envelope.json $jsonbkdir - ls -lt $jsonbkdir - + cp /tmp/${memberID}_config_update_as_envelope.pb $blockDir + # Stop configtxlator kill $configtxlator_pid - log "Created config update payload for the new organization '$NEW_ORG', in file /${DATA}/${NEW_ORG}_config_update_as_envelope.pb" + log "Created config update payload for the new organization '$memberID', in file /${blockDir}/${memberID}_config_update_as_envelope.pb" popd return 0 From 2ba317996971fb03d8b597dd72c5f8807d4cb2b0 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Thu, 20 Dec 2018 15:13:31 +0800 Subject: [PATCH 08/15] added part 5 --- new-member/create-config-update.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 new-member/create-config-update.sh diff --git a/new-member/create-config-update.sh b/new-member/create-config-update.sh old mode 100644 new mode 100755 From 6d7d4c1b6825d26f9b2ca2b621b4e21bab5be252 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Thu, 20 Dec 2018 18:57:35 +0800 Subject: [PATCH 09/15] added part 5 --- new-member/README.md | 188 +++++++++++++++++++++++++++-- new-member/create-config-update.sh | 58 ++++++--- new-member/s3-handler.sh | 6 +- ngo-fabric/README.md | 2 +- 4 files changed, 221 insertions(+), 33 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 505c6c5a..27e448f1 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -442,12 +442,16 @@ On the Fabric client node in Account A. This step generates a new channel configuration block that includes the new member owned by Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. +> For interest, 'genesis block' appears in two places in Fabric: +> 1) The orderer is bootstrapped using a genesis block, which is used to create the orderer system channel. The genesis block is created using this command: `configtxgen -outputBlock`, and is passed to the orderer on startup, usually via an ENV variable or parameter (named `General.GenesisFile`). The system channel name defaults to 'testchainid' unless you override it. +> 2) Application channels are created using a channel configuration block. The first of these becomes the genesis block for the channel. This is created using this command: `configtxgen -outputCreateChannelTx`. + A new channel configuration block is created by fetching the latest configuration block from the channel, generating a new channel configuration, then comparing the old and new configurations to generate a 'diff'. Generating a new channel configuration involves a number of steps, which we will work through below. -### Generate configtx channel configuration -Generate the configtx channel configuration by executing the following script: +### Generate new configtx.yaml channel creation configuration +Generate the configtx channel creation configuration by executing the following script: ``` docker exec cli configtxgen -outputCreateChannelTx /opt/home/$CHANNEL-two-org.pb -profile TwoOrgChannel -channelID $CHANNEL --configPath /opt/home/ @@ -466,11 +470,34 @@ You should see: Check that the channel configuration has been generated: ``` -ls -lt $CHANNEL-two-org.pb +ls -lt ~/$CHANNEL-two-org.pb +``` + +### Print the new member configuration +Generate a member definition for the new member. It reads the information for the member from configtx.yaml, created in the previous step: + +``` +export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +docker exec cli /bin/bash -c "configtxgen -printOrg $NEWMEMBERID --configPath /opt/home/ > /tmp/$NEWMEMBERID.json" ``` -### Fetch the latest config block from the channel -The genesis block for the channel exists here: +You should see: + +``` +2018-12-20 09:03:18.175 UTC [common/tools/configtxgen] main -> WARN 001 Omitting the channel ID for configtxgen is deprecated. Explicitly passing the channel ID will be required in the future, defaulting to 'testchainid'. +2018-12-20 09:03:18.175 UTC [common/tools/configtxgen] main -> INFO 002 Loading configuration +2018-12-20 09:03:18.177 UTC [common/tools/configtxgen/encoder] NewOrdererOrgGroup -> WARN 003 Default policy emission is deprecated, please include policy specificiations for the orderer org group m-TRD4XPJBOREM7BDMBV6WLG3NKM in configtx.yaml +``` + +Check that the new member configuration has been generated and that is has more than 0 bytes: + +``` +$ docker exec cli ls -lt /tmp/$NEWMEMBERID.json +-rw-r--r-- 1 root root 4678 Dec 20 09:03 /tmp/m-TRD4XPJBOREM7BDMBV6WLG3NKM.json +``` + +### Fetch the latest configuration block from the channel +The channel creation block for the channel exists in the location below. It was created just prior to the creation of the channel in [Part 1:](../ngo-fabric/README.md). ``` ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer @@ -489,27 +516,170 @@ Check that the latest config block file now exists: ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer ``` +### Create an updated channel config including the new member +We will use a bash script to create an updated channel config since the process involves quite a few steps. We need to execute the bash script in the CLI container, so we copy it to the home directory on the Fabric client node, as this is mounted into the CLI container. + +``` +cd ~/non-profit-blockchain/new-member +cp create-config-update.sh ~ +``` + +A utility program called 'configtxlator' is used to create the new channel config - it translates the JSON channel configurations to/from binary protobuf structures. 'configtxlator' can run as a REST API server, so we start it in this mode since we need to make a few calls to it. + +``` +docker exec -e "CHANNEL=mychannel" -e "MEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM" -e "BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer" cli /opt/home/create-config-update.sh +``` + +You should see: + +``` +$ docker exec -e "CHANNEL=mychannel" -e "MEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM" -e "BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer" cli /opt/home/create-config-update.sh +Creating config update payload for the new member 'm-TRD4XPJBOREM7BDMBV6WLG3NKM' +configtxlator_pid:983 +Sleeping 5 seconds for configtxlator to start... +2018-12-20 09:15:42.578 UTC [configtxlator] startServer -> INFO 001 Serving HTTP requests on 0.0.0.0:7059 +/tmp /opt/home + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 43033 0 30048 100 12985 963k 416k --:--:-- --:--:-- --:--:-- 978k +Checking whether member 'm-TRD4XPJBOREM7BDMBV6WLG3NKM' already exists in the channel config +About to execute jq '.channel_group.groups.Application.groups | contains({m-TRD4XPJBOREM7BDMBV6WLG3NKM})' +Member 'm-TRD4XPJBOREM7BDMBV6WLG3NKM' does not exist in the channel config. This is expected as we are about to add the member + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 31213 0 8844 100 22369 432k 1093k --:--:-- --:--:-- --:--:-- 1149k + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 40085 0 11271 100 28814 507k 1297k --:--:-- --:--:-- --:--:-- 1339k + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 23284 0 2635 100 20649 44324 339k --:--:-- --:--:-- --:--:-- 341k + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 8926 0 6291 100 2635 327k 137k --:--:-- --:--:-- --:--:-- 341k + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 7651 0 2661 100 4990 137k 258k --:--:-- --:--:-- --:--:-- 270k +Created config update payload for the new organization 'm-TRD4XPJBOREM7BDMBV6WLG3NKM', in file /opt/home/fabric-samples/chaincode/hyperledger/fabric/peer/m-TRD4XPJBOREM7BDMBV6WLG3NKM_config_update_as_envelope.pb +/opt/home +/opt/home/create-config-update.sh: line 92: 983 Terminated configtxlator start +``` + +Check that the update channel config block file now exists. You should see the files below. You have just created the file ending in `config_update_as_envelope.pb`. + +``` +$ ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer +total 36 +-rw-r--r-- 1 root root 2661 Dec 20 10:41 m-TRD4XPJBOREM7BDMBV6WLG3NKM_config_update_as_envelope.pb +-rw-r--r-- 1 root root 12985 Dec 20 10:40 mychannel.config.block +-rw-r--r-- 1 root root 12985 Dec 13 06:44 mychannel.block +``` ## Step 8: Endorsing peers must sign the new channel configuration -The 'diff' must be signed by the existing network members, i.e. the network members must endorse the changes to the channel configuration and approve the new member joining the channel. A channel configuration update is really just another transaction in Fabric, known as a 'configuration transaction', and as such it must be endorsed by network members in accordance with the modification policy for the channel. The default modification policy for the channel Application group is MAJORITY, which means a majority of members need to sign the channel configuration update. +On the Fabric client node in Account A. + +In this step, when we refer to 'diff', we mean the binary protobuf version of the 'diff' file, which we wrapped in an envelope in the final step in the script `create-config-update.sh`. This file is titled `m-TRD4XPJBOREM7BDMBV6WLG3NKM_config_update_as_envelope.pb` in the previous step, and can be found by executing this command: + +``` +ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer +``` + +The 'diff' created above must be signed by the existing network members, i.e. the network members must endorse the changes to the channel configuration and approve the new member joining the channel. A channel configuration update is really just another transaction in Fabric, known as a 'configuration transaction', and as such it must be endorsed by network members in accordance with the modification policy for the channel. The default modification policy for the channel Application group is MAJORITY, which means a majority of members need to sign the channel configuration update. + +Since our network currently contains only one member, the network creator, this step isn't strictly required as only the network creator would need to sign the package. However, I've made it an explicit step as you will need it when your Fabric network grows to 2 or more members. To allow admins belonging to different members to sign the channel configuration you will need to pass the channel configuration ‘diff’ file to each member in the network, one-by-one, and have them sign the channel configuration. Each member signature must be applied in turn so that we end up with a package that has the signatures of all endorsing members. Alternatively, you could send the channel config to all members simultaneously and wait to receive signed responses, but then you would have to extract the signatures from the individual responses and create a single package which contains the configuration update plus all the required signatures. -At this point you may only have two members: the member owned by the network creator, and the new member. In this case, only the member owned by the network creator needs to sign the package. However, I have made this step explicit because you will need to -include this step as more members join the network. +``` +export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +export BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli bash -c "peer channel signconfigtx -f ${BLOCKDIR}/${NEWMEMBERID}_config_update_as_envelope.pb" +``` + +If you compare the size of the file now you will see it is larger. This is due to the digital signature that is now included: + +``` +ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer +``` Once we have a signed channel configuration we can apply it to the channel. ## Step 9: Account A updates the channel with the new configuration +On the Fabric client node in Account A. + In this step we update the channel with the new channel configuration. Since the new channel configuration now includes details of the new organisation, this will allow the new organisation to join the channel. +``` +export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +export BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli bash -c "peer channel update -f ${BLOCKDIR}/${NEWMEMBERID}_config_update_as_envelope.pb -c $CHANNEL -o $ORDERER --cafile /opt/home/managedblockchain-tls-chain.pem --tls" +``` + +You should see: + +``` +2018-12-20 10:45:10.899 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized +2018-12-20 10:45:10.988 UTC [channelCmd] update -> INFO 002 Successfully submitted channel update +``` + +You can now join a peer owned by the new member to the channel. + ## Step 10: Account A shares the genesis block for the channel with Account B -Before the peer node in Account B joins the channel, it must be able to connect to the Ordering Service managed by Amazon Managed Blockchain. It obtains the Ordering Service endpoint from the channel genesis block. The file mychannel.block ('mychannel' refers to the channel name and may differ if you have changed the channel name) would have been created when you first created the channel. Make sure the mychannel.block file is available to the peer node in Account B. +On the Fabric client node in Account A. + +Before the peer node in Account B joins the channel, it must be able to connect to the Ordering Service managed by Amazon Managed Blockchain. The peer obtains the Ordering Service endpoint from the channel genesis block. The file mychannel.block ('mychannel' refers to the channel name and may differ if you have changed the channel name) would have been created when you first created the channel in [Part 1](../ngo-fabric/README.md). Make sure the mychannel.block file is available to the peer node in Account B. + +Copy the channel genesis from from Account A to S3: + + +```bash +cd ~/non-profit-blockchain +./new-member/s3-handler.sh createS3BucketForCreator +./new-member/s3-handler.sh copyChannelGenesisToS3 +``` + +On the Fabric client node in Account B. + +```bash +cd ~/non-profit-blockchain +./new-member/s3-handler.sh copyChannelGenesisFromS3 +``` + +On the EC2 bastion in the new org. +```bash +cd +cd hyperledger-on-kubernetes +./remote-org/scripts/copy-tofrom-S3.sh copyChannelGenesisFromS3 +ls -l /opt/share/rca-data/mychannel.block +``` ## Step 11: Account B starts its peer node and joins the channel +On the Fabric client node in Account B. + The next step is to join the peer node to the channel. After the peer successfully joins the channel it will start receiving blocks of transactions and build its own copy of the ledger, creating the blockchain and populating the world state key-value store. +Join peer to Fabric channel. + +Execute the following script: + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer channel join -b $CHANNEL.block -o $ORDERER --cafile $CAFILE --tls +``` + +You should see: + +``` +2018-11-26 21:41:40.983 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized +2018-11-26 21:41:41.022 UTC [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel +``` + ## Step 12: Account B installs chaincode After install the chaincode, the peer node in Account B will be able to run queries and endorse transactions. diff --git a/new-member/create-config-update.sh b/new-member/create-config-update.sh index bf175ef4..4a4bcc3d 100755 --- a/new-member/create-config-update.sh +++ b/new-member/create-config-update.sh @@ -13,57 +13,75 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. -region=us-east-1 -memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM -blockDir=/home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer - function createConfigUpdate { - log "Creating config update payload for the new member '$memberID'" + echo "Creating config update payload for the new member '$MEMBERID'" + cd /opt/home # Start the configtxlator configtxlator start & configtxlator_pid=$! - log "configtxlator_pid:$configtxlator_pid" - log "Sleeping 5 seconds for configtxlator to start..." + echo "configtxlator_pid:$configtxlator_pid" + echo "Sleeping 5 seconds for configtxlator to start..." sleep 5 pushd /tmp CTLURL=http://127.0.0.1:7059 # Convert the config block protobuf to JSON - curl -X POST --data-binary @$blockDir/$CHANNEL.config.block $CTLURL/protolator/decode/common.Block > ${memberID}_config_block.json + curl -X POST --data-binary @$BLOCKDIR/$CHANNEL.config.block $CTLURL/protolator/decode/common.Block > ${MEMBERID}_config_block.json # Extract the config from the config block - jq .data.data[0].payload.data.config ${memberID}_config_block.json > ${memberID}_config.json + jq .data.data[0].payload.data.config ${MEMBERID}_config_block.json > ${MEMBERID}_config.json - isOrgInChannelConfig ${memberID}_config.json + isMemberInChannelConfig ${MEMBERID}_config.json if [ $? -eq 0 ]; then - log "Org '$memberID' already exists in the channel config. Config will not be updated. Exiting createConfigUpdate" + echo "Member '$MEMBERID' already exists in the channel config. Config will not be updated. Exiting createConfigUpdate" return 1 fi # Append the new org configuration information - jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"'$memberID'":.[1]}}}}}' ${memberID}_config.json ${memberID}.json > ${memberID}_updated_config.json + jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"'$MEMBERID'":.[1]}}}}}' ${MEMBERID}_config.json ${MEMBERID}.json > ${MEMBERID}_updated_config.json # Create the config diff protobuf - curl -X POST --data-binary @${memberID}_config.json $CTLURL/protolator/encode/common.Config > ${memberID}_config.pb - curl -X POST --data-binary @${memberID}_updated_config.json $CTLURL/protolator/encode/common.Config > ${memberID}_updated_config.pb - curl -X POST -F original=@${memberID}_config.pb -F updated=@${memberID}_updated_config.pb $CTLURL/configtxlator/compute/update-from-configs -F channel=$CHANNEL_NAME > ${memberID}_config_update.pb + curl -X POST --data-binary @${MEMBERID}_config.json $CTLURL/protolator/encode/common.Config > ${MEMBERID}_config.pb + curl -X POST --data-binary @${MEMBERID}_updated_config.json $CTLURL/protolator/encode/common.Config > ${MEMBERID}_updated_config.pb + curl -X POST -F original=@${MEMBERID}_config.pb -F updated=@${MEMBERID}_updated_config.pb $CTLURL/configtxlator/compute/update-from-configs -F channel=$CHANNEL > ${MEMBERID}_config_update.pb # Convert the config diff protobuf to JSON - curl -X POST --data-binary @${memberID}_config_update.pb $CTLURL/protolator/decode/common.ConfigUpdate > ${memberID}_config_update.json + curl -X POST --data-binary @${MEMBERID}_config_update.pb $CTLURL/protolator/decode/common.ConfigUpdate > ${MEMBERID}_config_update.json # Create envelope protobuf container config diff to be used in the "peer channel update" command to update the channel configuration block - echo '{"payload":{"header":{"channel_header":{"channel_id":"'"${CHANNEL}"'", "type":2}},"data":{"config_update":'$(cat ${memberID}_config_update.json)'}}}' > ${memberID}_config_update_as_envelope.json - curl -X POST --data-binary @${memberID}_config_update_as_envelope.json $CTLURL/protolator/encode/common.Envelope > /tmp/${memberID}_config_update_as_envelope.pb + echo '{"payload":{"header":{"channel_header":{"channel_id":"'"${CHANNEL}"'", "type":2}},"data":{"config_update":'$(cat ${MEMBERID}_config_update.json)'}}}' > ${MEMBERID}_config_update_as_envelope.json + curl -X POST --data-binary @${MEMBERID}_config_update_as_envelope.json $CTLURL/protolator/encode/common.Envelope > /tmp/${MEMBERID}_config_update_as_envelope.pb # copy to the /data directory so the file can be signed by other admins - cp /tmp/${memberID}_config_update_as_envelope.pb $blockDir + cp /tmp/${MEMBERID}_config_update_as_envelope.pb $BLOCKDIR # Stop configtxlator kill $configtxlator_pid - log "Created config update payload for the new organization '$memberID', in file /${blockDir}/${memberID}_config_update_as_envelope.pb" + echo "Created config update payload for the new organization '$MEMBERID', in file ${BLOCKDIR}/${MEMBERID}_config_update_as_envelope.pb" popd return 0 } +# Checks whether the new member already exists in the channel config. This would be true if the member has already been added +# to the channel config +function isMemberInChannelConfig { + if [ $# -ne 1 ]; then + echo "Usage: isMemberInChannelConfig " + exit 1 + fi + echo "Checking whether member '$MEMBERID' already exists in the channel config" + local JSONFILE=$1 + + # check if the member exists in the channel config + echo "About to execute jq '.channel_group.groups.Application.groups | contains({$MEMBERID})'" + if cat ${JSONFILE} | jq -e ".channel_group.groups.Application.groups | contains({\"$MEMBERID\"})" > /dev/null; then + echo "Member '$MEMBERID' already exists in the channel config" + return 0 + else + echo "Member '$MEMBERID' does not exist in the channel config. This is expected as we are about to add the member" + return 1 + fi +} + createConfigUpdate diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index 683b200d..3c1465ac 100755 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -56,7 +56,7 @@ function copyCertsFromS3 { function copyChannelGenesisToS3 { echo "Copying the Channel Genesis block to S3" if [[ $(aws configure list) && $? -eq 0 ]]; then - aws s3api put-object --bucket $S3BucketNameCreator --key org0/mychannel.block --body ${DATA}/mychannel.block + aws s3api put-object --bucket $S3BucketNameCreator --key org0/mychannel.block --body /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block aws s3api put-object-acl --bucket $S3BucketNameCreator --key org0/mychannel.block --grant-read uri=http://acs.amazonaws.com/groups/global/AllUsers aws s3api put-object-acl --bucket $S3BucketNameCreator --key org0/mychannel.block --acl public-read else @@ -69,8 +69,8 @@ function copyChannelGenesisToS3 { function copyChannelGenesisFromS3 { echo "Copying the Channel Genesis block from S3" if [[ $(aws configure list) && $? -eq 0 ]]; then - sudo chown ec2-user ${DATA}/mychannel.block - aws s3api get-object --bucket $S3BucketNameCreator --key org0/mychannel.block ${DATA}/mychannel.block + sudo chown ec2-user /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block + aws s3api get-object --bucket $S3BucketNameCreator --key org0/mychannel.block /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block else echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" fi diff --git a/ngo-fabric/README.md b/ngo-fabric/README.md index ec4d8731..48a02107 100644 --- a/ngo-fabric/README.md +++ b/ngo-fabric/README.md @@ -206,7 +206,7 @@ cp ~/non-profit-blockchain/ngo-fabric/configtx.yaml ~ vi ~/configtx.yaml ``` -Generate the configtx channel configuration by executing the following script: +Generate the configtx channel configuration by executing the following script. When the channel is created, this channel configuration will become the genesis block (i.e. block 0) on the channel: ``` docker exec cli configtxgen -outputCreateChannelTx /opt/home/$CHANNEL.pb -profile OneOrgChannel -channelID $CHANNEL --configPath /opt/home/ From b05ce5853f953e07a3b3e6e5884e604a46addafa Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Thu, 20 Dec 2018 19:01:50 +0800 Subject: [PATCH 10/15] added part 5 --- new-member/README.md | 9 +-------- new-member/s3-handler.sh | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 27e448f1..fe1f73f0 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -648,14 +648,7 @@ On the Fabric client node in Account B. ```bash cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyChannelGenesisFromS3 -``` - -On the EC2 bastion in the new org. -```bash -cd -cd hyperledger-on-kubernetes -./remote-org/scripts/copy-tofrom-S3.sh copyChannelGenesisFromS3 -ls -l /opt/share/rca-data/mychannel.block +ls -l /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block ``` ## Step 11: Account B starts its peer node and joins the channel diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index 3c1465ac..8e43ad02 100755 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -69,7 +69,7 @@ function copyChannelGenesisToS3 { function copyChannelGenesisFromS3 { echo "Copying the Channel Genesis block from S3" if [[ $(aws configure list) && $? -eq 0 ]]; then - sudo chown ec2-user /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block + sudo chown -R ec2-user /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer aws s3api get-object --bucket $S3BucketNameCreator --key org0/mychannel.block /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block else echo "AWS CLI is not configured on this node. To run this script install and configure the AWS CLI" From 0ddc8d3a63d3236130459f82fae4d2dc4d079140 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 21 Dec 2018 13:03:07 +0800 Subject: [PATCH 11/15] added part 5 --- new-member/README.md | 158 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 2 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index fe1f73f0..94bbd4da 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -674,10 +674,164 @@ You should see: ``` ## Step 12: Account B installs chaincode +On the Fabric client node in Account B. + After install the chaincode, the peer node in Account B will be able to run queries and endorse transactions. -## Step 13: Account B queries chaincode and invokes transactions -To check that the new member owned by Account B is a full member of the Fabric network, the peer node will execute queries against its own ledger and invoke transactions on the network. Since the new member is included in the endorsement policy we expect to this transactions endorsed by the new member. +Install chaincode on Fabric peer. + +Execute the following script: + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode install -n $CHAINCODENAME -v $CHAINCODEVERSION -p $CHAINCODEDIR +``` + +You should see: + +``` +2018-11-26 21:41:46.585 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc +2018-11-26 21:41:46.585 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc +2018-11-26 21:41:48.004 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response: +``` + +## Step 13: Account B queries the chaincode +On the Fabric client node in Account B. + +Query the chaincode on Fabric peer. + +Execute the following script: + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode query -C $CHANNEL -n $CHAINCODENAME -c '{"Args":["query","a"]}' +``` + +You should see the latest balance for account holder 'a'. If you have executed only the single 'invoke' transaction in Account A when you setup the Fabric network, the balance would be '90'. If you executed more than one 'invoke', the balance would have been reduced by 10 each time, so it will be some value less than 90. + +``` +90 +``` + +## Step 14: Account B invokes a transaction +On the Fabric client node in Account B. + +Invoke a Fabric transaction. + +Execute the following script: + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode invoke -o $ORDERER -C $CHANNEL -n $CHAINCODENAME \ + -c '{"Args":["invoke","a","b","10"]}' --cafile $CAFILE --tls +``` + +You should see: + +``` +2018-11-26 21:45:20.935 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 +``` + +However, if you query the balance again you will find the balance has not changed. Any idea why? + +Your Account B peer node is now a committer peer, which means the peer is able to validate the blocks it receives and maintain its own ledger. However, it is not yet an endorsing peer and cannot take part in endorsing transactions. The `docker exec` statement we used above sends the transaction to the Account B peer node for endorsement. The Account B peer node will follow the standard Fabric process of simulating the transaction and endorsing it. The endorsed transaction is then sent to the ordering service, which groups transactions into blocks and sends to the peer nodes. Each peer node will then validate the transactions in the block. The transaction endorsed by Account B will fail the validation step as it does not meet the endorsement policy of the chaincode on this channel. The transaction will be written to the blockchain on each peer as an 'invalid' transaction, while the world state will not be updated. + +You can check the block height on the channel to confirm that the transactions you invoke do result in new blocks, even though there is no update to the world state. Run this before and after you run the `peer chaincode invoke`. + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer channel getinfo -o $ORDERER -c $CHANNEL --cafile $CAFILE --tls +``` + +We can resolve the invalid transaction in two ways: + +1. Send the `peer chaincode invoke` transaction to peer nodes in Account A and Account B. Since Account A is able to endorse transactions, the statement below should work. It will obtain endorsements from peers in Account A and Account B. When the endorsement policy is checked during the validation step, it will succeed as the transaction has an endorsement from a member included in the policy. If you review [Part 1](../ngo-fabric/README.md) where we instantiated the chaincode, we did not provide an endorsement policy, so the default endorsement policy would have been applied. In this case it would be `“OR(‘Org1.member’)”`. + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" cli peer chaincode invoke -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -c '{"Args":["invoke","a","b","10"]}' --cafile $CAFILE --tls --peerAddresses nd-JZMD3XB7R5ARXA5HVM4GPZ2CFI.m-TEW3EJGTPBBW7BMGXYOIXV5364.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30003 --peerAddresses nd-KSQ2ACC3PZBNBNBPYWKUEAEABY.m-TRD4XPJBOREM7BDMBV6WLG3NKM.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30006 --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem +``` + +2. Add the new member to the endorsement policy. To do this, the chaincode on the channel needs to be updated with a new endorsement policy that includes the new member. This will be done in the next step. + +## Step 15: Account A updates the endorsement policy for the channel +On the Fabric client node in Account A. + +Upgrade the chaincode on the Fabric channel. This requires us to increment the chaincode version, which we do using the `export` statement below, then install a new version of the chaincode. The chaincode itself is not changing, only the version number and the endorsement policy applied to it on this channel. + +Execute the following script: + +``` +export CHAINCODEVERSION=v2 +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode install -n $CHAINCODENAME -v $CHAINCODEVERSION -p $CHAINCODEDIR +``` + +You should see: + +``` +2018-12-21 04:38:18.006 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc +2018-12-21 04:38:18.006 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc +2018-12-21 04:38:18.130 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response: +``` + +Then upgrade the new chaincode on the channel. This is very similar to instantiating chaincode, and should take around 30 seconds to complete as Fabric creates a new Docker chaincode container, installs the chaincode, and calls the 'init' function. Keep this in mind when you design your own chaincode: the 'init' function is going to run each time the chaincode is upgraded, so don't make the mistake made in the fabric-samples where the state is initialised in the init function. Best practice for Fabric is to have a separate function to initialise the state, and only call this once, during chaincode instantiation. + +Before running this, change the members in your endorsement policy to match your own member IDs. You can find the member IDs for both Account A and Account B in the Amazon Managed Blockchain console, or you can look in Step 7 above where you added both member IDs to configtx.yaml, in the section `Organizations->Name`. + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode upgrade -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -v $CHAINCODEVERSION \ + -c '{"Args":["init","a","100","b","200"]}' --cafile $CAFILE --tls -P "OR('m-TEW3EJGTPBBW7BMGXYOIXV5364.member','m-TRD4XPJBOREM7BDMBV6WLG3NKM.member')" +``` + +You should see: + +``` +2018-12-21 04:38:26.615 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc +2018-12-21 04:38:26.615 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc +``` + +Now query and invoke transactions to confirm that the upgrade was successful. + +## Step 16: Account B updates the endorsement policy for the channel +On the Fabric client node in Account B. + +If you run the query statement again it will fail. This is because the peer node in Account B does not have the latest version of the chaincode: + +``` +$ docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ +> -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ +> cli peer chaincode query -C $CHANNEL -n $CHAINCODENAME -c '{"Args":["query","a"]}' +Error: endorsement failure during query. response: status:500 message:"cannot retrieve package for chaincode mycc/v2, error open /var/hyperledger/production/chaincodes/mycc.v2: no such file or directory" +``` + +Execute the following script to install the latest version of the chaincode: + +``` +export CHAINCODEVERSION=v2 +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode install -n $CHAINCODENAME -v $CHAINCODEVERSION -p $CHAINCODEDIR +``` + +Run the query again. It should take around 30 seconds as a new Docker image is prepared with the latest version of the chaincode. + +Now for the final test - check whether the peer node on Account B can invoke a transaction and endorse it: + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode invoke -o $ORDERER -C $CHANNEL -n $CHAINCODENAME \ + -c '{"Args":["invoke","a","b","10"]}' --cafile $CAFILE --tls +``` + +Rerunning the query should confirm that the peer node in Account B can now endorse transactions. The validation checks performed on each peer node have confirmed that a transaction endorsed by Account B is valid, and the world state on each peer will be updated. You can double check this by invoking transactions on peer nodes in Account A or Account B, then running the same query on peer nodes in Account A and Account B, and you will see the same results on both peers. ## The workshop sections The workshop instructions can be found in the README files in parts 1-4: From 5b8530d1d1dcf9f8d6d42d1528a7ae8a8b2c0a1e Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Fri, 21 Dec 2018 13:18:47 +0800 Subject: [PATCH 12/15] added part 5 --- new-member/README.md | 50 ++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 94bbd4da..0179204b 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -5,16 +5,26 @@ node to the channel created in [Part 1](../ngo-fabric/README.md). The new peer n on the next channel and will build its own copy of the ledger. We will also configure the Fabric network so the new member can take part in endorsing transactions. -Adding a new member to an existing Fabric network involves a number of steps. The new member is located in a different AWS account, and the steps therefore involve cooperation between administrators for the Fabric member in the existing account (let’s call this Account A), and the new account (let’s call this Account B). The steps look as follows: +Adding a new member to an existing Fabric network involves a number of steps. The new member is located in a different AWS account, and the steps therefore involve cooperation between administrators for the Fabric member in the existing creator account (let’s call this Account A), and the new account (let’s call this Account B). The steps look as follows: 1. Account A invites Account B to join the Fabric network 2. Account B creates a member in the Fabric network 3. Account B creates a peer node -4. Account B shares the public keys for its member with Account A -5. Account A updates the channel configuration with the MSP for Account B -6. Account A shares the genesis block for the channel with Account B -7. Account B starts its peer node and joins the channel -8. If Account B will take part in endorsing transaction proposals, Account B will install chaincode +4. Account B creates a Fabric client node +5. Account B prepares the Fabric client node and enrolls an identity +6. Account B shares the public keys for its member with Account A +7. Account A creates an MSP for the new Account A member +8. Account A updates the configtx.yaml configuration with the MSP for Account B +9. Account A updates the channel configuration with the MSP for Account B +10. Endorsing peers sign the new channel configuration +11. Account A updates the channel with the new configuration +12. Account A shares the genesis block for the channel with Account B +13. Account B starts its peer node and joins the channel +14. Account B installs chaincode +15. Account B queries the chaincode +16. Account B invokes a transaction +17. Account A updates the endorsement policy for the chaincode on the channel +18. Account B installs the latest version of the chaincode ## Pre-requisites - Account A, the network creator There are multiple parts to the workshop. Before starting on Part 5, a network creator should haved completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. @@ -141,7 +151,7 @@ Check the progress in the AWS CloudFormation console and wait until the stack is You will find some useful information in the Outputs tab of the CloudFormation stack once the stack is complete. We will use this information in later steps. -## Step 4 - Account B prepares the Fabric client node and enrolls an identity +## Step 5: Account B prepares the Fabric client node and enrolls an identity On the Fabric client node. Prior to executing any commands in the Fabric client node, you will need to export ENV variables @@ -227,7 +237,7 @@ mkdir -p /home/ec2-user/admin-msp/admincerts cp ~/admin-msp/signcerts/* ~/admin-msp/admincerts/ ``` -## Step 5: Account B shares the public keys for its member with Account A +## Step 6: Account B shares the public keys for its member with Account A On the Fabric client node in Account B. Account B needs to share two certificates with Account A: @@ -252,7 +262,7 @@ cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsToS3 ``` -## Step 6: Account A creates an MSP for the new Account A member +## Step 7: Account A creates an MSP for the new Account A member On the Fabric client node in Account A. Account A stores the certificates provided by Account B on its Fabric client node. @@ -272,7 +282,7 @@ cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsFromS3 ``` -## Step 7: Account A updates the configtx.yaml configuration with the MSP for Account B +## Step 8: Account A updates the configtx.yaml configuration with the MSP for Account B On the Fabric client node in Account A. The configtx.yaml file contains details of the organisations in a Fabric network as well as channel configuration profiles that can be used when creating new channels. The channel creator originally created this file just before creating the channel. The channel creator now needs to add the new member to this file. @@ -437,7 +447,7 @@ Profiles: - *Org2 ``` -## Step 7: Account A updates the channel configuration with the MSP for Account B +## Step 9: Account A updates the channel configuration with the MSP for Account B On the Fabric client node in Account A. This step generates a new channel configuration block that includes the new member owned by Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. @@ -575,7 +585,7 @@ total 36 -rw-r--r-- 1 root root 12985 Dec 13 06:44 mychannel.block ``` -## Step 8: Endorsing peers must sign the new channel configuration +## Step 10: Endorsing peers must sign the new channel configuration On the Fabric client node in Account A. In this step, when we refer to 'diff', we mean the binary protobuf version of the 'diff' file, which we wrapped in an envelope in the final step in the script `create-config-update.sh`. This file is titled `m-TRD4XPJBOREM7BDMBV6WLG3NKM_config_update_as_envelope.pb` in the previous step, and can be found by executing this command: @@ -606,7 +616,7 @@ ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer Once we have a signed channel configuration we can apply it to the channel. -## Step 9: Account A updates the channel with the new configuration +## Step 11: Account A updates the channel with the new configuration On the Fabric client node in Account A. In this step we update the channel with the new channel configuration. Since the new channel configuration now includes details @@ -629,7 +639,7 @@ You should see: You can now join a peer owned by the new member to the channel. -## Step 10: Account A shares the genesis block for the channel with Account B +## Step 12: Account A shares the genesis block for the channel with Account B On the Fabric client node in Account A. Before the peer node in Account B joins the channel, it must be able to connect to the Ordering Service managed by Amazon Managed Blockchain. The peer obtains the Ordering Service endpoint from the channel genesis block. The file mychannel.block ('mychannel' refers to the channel name and may differ if you have changed the channel name) would have been created when you first created the channel in [Part 1](../ngo-fabric/README.md). Make sure the mychannel.block file is available to the peer node in Account B. @@ -651,7 +661,7 @@ cd ~/non-profit-blockchain ls -l /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block ``` -## Step 11: Account B starts its peer node and joins the channel +## Step 13: Account B starts its peer node and joins the channel On the Fabric client node in Account B. The next step is to join the peer node to the channel. After the peer successfully joins the channel it will start receiving blocks of transactions and build its own copy of the ledger, creating the blockchain and populating the world state key-value store. @@ -673,7 +683,7 @@ You should see: 2018-11-26 21:41:41.022 UTC [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel ``` -## Step 12: Account B installs chaincode +## Step 14: Account B installs chaincode On the Fabric client node in Account B. After install the chaincode, the peer node in Account B will be able to run queries and endorse transactions. @@ -696,7 +706,7 @@ You should see: 2018-11-26 21:41:48.004 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response: ``` -## Step 13: Account B queries the chaincode +## Step 15: Account B queries the chaincode On the Fabric client node in Account B. Query the chaincode on Fabric peer. @@ -715,7 +725,7 @@ You should see the latest balance for account holder 'a'. If you have executed o 90 ``` -## Step 14: Account B invokes a transaction +## Step 16: Account B invokes a transaction On the Fabric client node in Account B. Invoke a Fabric transaction. @@ -757,7 +767,7 @@ docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt 2. Add the new member to the endorsement policy. To do this, the chaincode on the channel needs to be updated with a new endorsement policy that includes the new member. This will be done in the next step. -## Step 15: Account A updates the endorsement policy for the channel +## Step 17: Account A updates the endorsement policy for the chaincode on the channel On the Fabric client node in Account A. Upgrade the chaincode on the Fabric channel. This requires us to increment the chaincode version, which we do using the `export` statement below, then install a new version of the chaincode. The chaincode itself is not changing, only the version number and the endorsement policy applied to it on this channel. @@ -799,7 +809,7 @@ You should see: Now query and invoke transactions to confirm that the upgrade was successful. -## Step 16: Account B updates the endorsement policy for the channel +## Step 18: Account B installs the latest version of the chaincode On the Fabric client node in Account B. If you run the query statement again it will fail. This is because the peer node in Account B does not have the latest version of the chaincode: From f849f198e42237666d0aa147e8b6f4bca953715c Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Wed, 9 Jan 2019 13:27:06 +0800 Subject: [PATCH 13/15] add part 5 --- README.md | 1 + images/MultimemberNetwork.png | Bin 0 -> 116463 bytes new-member/README.md | 67 +++++++++++++++++++++++++--------- ngo-chaincode/README.md | 1 + ngo-fabric/README.md | 1 + ngo-rest-api/README.md | 1 + ngo-ui/README.md | 1 + 7 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 images/MultimemberNetwork.png diff --git a/README.md b/README.md index e7486b10..fd836194 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ README instructions in parts 1-4, in this order: * [Part 2:](ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](ngo-rest-api/README.md) Run the REST API. * [Part 4:](ngo-ui/README.md) Run the Application. +* [Part 5:](new-member/README.md) Add a new member to the network. ## License diff --git a/images/MultimemberNetwork.png b/images/MultimemberNetwork.png new file mode 100644 index 0000000000000000000000000000000000000000..25e48c9230a233794efdcac04e2b1d3ee643d82d GIT binary patch literal 116463 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

wb<97QIXZQ0^_&K3GAs)f$oV!(2K{EC-d z_Kj^rpO2Yd)swJPVX~7?((TN@30k44qdN134P-+(NHR8A!(|s<1y^Q)EX8`UmK-76 z70AD}EB9gB~GKJpWyv&^r zV@2D0GNYASd9>5wbX|SkuGNI?k$hiFy5ph+bJF*1$~@W@sb3xGPl^7>F(bNi!y8i@ ziT;*5eRt`zWOB-Zm@6A1_MD%)DmIR$d1-p*Q_fYTP(LItk#7b)W-{eXQLRB=&Oi%gpMd?f9r@Q7_oqS)gjAOC_~I%I;Nc`eVaPgmF~ zM5o;Ks0tdgyH`oJBTxH@&h9tL%o)`2y!$NarqHLQw&NPNh=RKJj7PGbJdw3jjMA~~5l6&A$@VrW zG}gI*XshNLxkA%$2Z`>AP^R4ikzk+i#f)2j9}pA9n(`U_G>T?-jfJPeyj*dGHrO>P zWMcFZbt+O|hH}!gpp1*~aj*}cvoEcnf*JNY#4m+Cw9kb@JN}Y7p@2e?11! zH!2xD!dZk7`O#s^k7ohM+^jEZ_tJPzle(#C$)!aQmEnC~+5@802!qYIXvz-*t!zWnj3A6$4O|yZ>diTQAf(R$0BaYWOax9XP=AFCaUgh{}i$U zz53#&_V{q5_u`P{4|+k&wP~n640^LZTT}7;G&p&KFvr8HgofAhNlryOKb3xdB30n= znB%lfWtoOTE#GM$ZhhL~=axY%Cy8L%^*qo=XBSGHC2k2-X}EQME!Wss8b%wH+kY9( zADTv%j+9?~xR*YQio3w0c_!aPl)Bk>Y;&DCd)0~&A;5z(#9C|~@0IS=RuWOWNVL3X zH%hS)PcBKCCD>y z&-&H3%*4b0%sWzm12Gkv7hJg~;l#(bbg9+hO{&e88wIq}-po|4#&OlqSOGB|noAYe z(etSN5Q4ofLP)Ber;OqEWGgZ@1o3yx*#u{_?*(b(^6EHE`O0lfM~+i_EJo&f25?qD z7ki0T&GxY7SpLE<7!Ll&4p%S9x%#~k_<@sNk+@&WQ2yXe zv-yv=@qHX)Zy7|iz5FR^7`Ay)+ai3|@|oLhxn%p0`OZQ|+n=`PPUE7IAtA^h)Udsg zd*NU$bR3sL=I09e*|$MmBDm~SAUP=bU|BBU3t#z@)7M~UTR3Z(&FOsfz|!aC*{^H9 z?bD?17-YBzq^wHGH{wRHSSai7Y&&rZ<+W)I>~%xLD{szClt!yFhhqYgu=jL+TZVJR zeBCJLi``|V>}QxV6&^_eOJJ3^HN+}=v56OOlz20g!Jn#QfN$j@RMlH%vLZDLr1dgUi2(-8e>8Q6E5abBA7%Eoi_xmZC!)>|9oUU?X z@KuY|hlgUYm*e(`HCsq+^wro}VUj?-lj8{=d-KP^w=|y|12lr#w=HGA7|GMj7~yvd z|GcJDJCn5I@R;DXbZ)BC7c^Is`u$2P`GPmW;8Ztd~I2RR;i+J%I-zab00LAvD?Q;g1Fm!}#i#UtwmBY4N!5Se%Ns({1 zIqD+<8dmOMkrHe{LWp04zOl%J(SAXUFxin!cYBW&MtgcI9q@Rgptz&mN}u9ijp_fs zMg%)BdiM8!HD3PXItuZtg9wiCN6mJ5oM_2CDNxnh<@T-8()xD zZ_3HjmH)ZC6rpbhbj(<2X=-PR#{U>~`>}!Bv%-PiFwuD1@7|n7B?2x4+(*@s+)wmq zt;zS26+RW=KLQQ#QEp&h#QATZRT}Uqq2yLIqW?7#Lv�nf)A?@ZTeAC<*pr8uLop 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

;05T?z- zYzJECy|W}#GJGPr*XZ0l%zXR4TZbd!s%?TQ@DHad>Sd8GnJ|hFw~Yn}(d9WIQ$xD| za>9K{WP)i~v3w?=@-{~TP8lWSx;fYmn)Oy3hL!w7rA1C0=oq&ewtlm8VCYDh2oXH= z6{(-?6eDNY2gk*F;JV~IB7Z2uPr0R6cmt#!3F1T)pGXBmr#WHHk_O*Q?=D6ih3+OG zcipLqi10}_cf0hPN~^m2CrRw)J#<<>B@y}?l384)y{Ex-NwH=<^h-geT0Lf`=V$Yt z%B`W&TfGlqU*{fPc~?%==TV~Ro&Y`yPljZoTNFf6g-O{ayt3T|ecl%1XY&P>CHu3W z717PXbhCw!nGUK29$&JZ=fov0PWsZPKJ!TSMr0Zvh_Dxda;l~SryY2r?@uW)cFR7C zq+*2Khi7u`(US}IzVL!e#%2d1BprW$YjHptGQOE|>v>aahSh?UXzi}}9gwIY{{{)% z=K-%w#=F_s^4^2gG5c{FpQl>I4G>GPv3EjXw zq{4Q0UrR@F$FAJmUvR(ty?QbIJ8CH_Y#tHmsq{ziW)pym82Z;=Xz9hOTChZWyn&GU z6U~H)Xd|lDn^WvH@*6>x9HVfAH#<1o=x7=^A-Xn$36OeN`4274^2Q_UH7{<4-S2Z1 z+m*RP2d(@+5`sEl+=$Ud#i+@EUc65ur7jd_%`N7XRrN<~EOnqHqwq+1FCvYRF6bfN z$0);V1E>YW%QQ2@k=*_hUO>WII!%if3x!oq@_O_VBiOWiX}IuCrk)Pcg<(eU*`5o< z&W&X-%r-_BKkN zJ*9|#E+}xEoweYBj{1_wb}7WwHNGtChQ$>*{CwFsIVwV$v((h>v;U?mP3u0kcuFl$ zV!qjQqKSM$851RW{7Fg4fMYc10%tO}^7uQ5Y`207b7TIn&AcwT9_sy~Ct+iQC}Wm& zaD=E)*~J?R{MsRZjw=qD2ps-AUziPNX1sh#mLyNfm0wtGh15Y};tIuG)(Gx1;+BuB zvgq5ax|`OxmFiU$@hd$t6*d0dOPeE z)wSMq-+FqQ>savU%fDOz|L_`ZWO-lklPqWe84eFRufE}7OPp4t!+Srde1)q5bCm%L-icwJkC=6 zE4KscK5aDah|qVzZ@Xa-f9pmhxTfQ?ZFFc@IVaDPfNU?7eruY?zIcHN4Af>CK(H0k z=sOYMAO|dr7e#JT)IwNqb&{${DUl4t#05YlL5(Jq9H!Z3Wp%u`H=InAH5h*x+D0Fs zebCuO+iK3ka(xxpCC?zxMBLo=4-oy`aDU&?@PWi~G)by|QrrI~xc_^V04VmW__?O4d;B&y5jh2r! z;HV*?G1EX211S~w2ZFawlwgF|yLi}}hJDWlrtsiv1#XAm>SewW+LGy8vUH{Eg zYMBR?*^8;&m)ELlR14MCx44dr#PgcBg)a84|p)q?3h$g zi+>2>w))X%jarT-)Rw-biH*+Ly*3@428c7TCYwS2PO4F+1O`q}$E&vFl zS1D73HS|>z3^g=9;|%PCSej+6I@u3Vaj)}a<0f8ns@a2tHewsUPQx!NiV;^e^VJcL za-7)3X-X(oeOOhry#ML=6B=rK#pi>k+@Y4VK)TRt5l}S0?Soz2CXda|F}0;IY_I)* zv4k*tgC<8wM+@&5%B)9kPG2$RM>bUGR??l5mlyFjS~+d6n8J_AJWr>b7E={tR_8yO z;vRUwpA$4oKD*&eoVhPzbn;Ab{^OEZlK%;%r;-umj;iGkW5(O()EA^LmEWy?;<<~L zm?MCCbmauVM7op&Vm3)&*72ez&Wsu1F;;~WfsyMIL_YH4uV>*fdiutOA;I~P!uVKLE-SUKxS`j~2Sv|WUWNQYEvc;-u zQp9fR5>s~i`7aphQVi#cW2LW|OT5oRCrX*|?{zXVa&W}`H}C!vB`{B8h*IwHzUS0t z+|Jo~(odeFxs_im=Q?FU-=k1ui&~Etc2;_wWyeVo8~gZMz;=3oaKnrl!Ta%E3-=MW zw8BE^}&_wQ+FdG(!e-U;4{O#hAHEGl*Msiknl178`1er$pK*nb-*yleb7 zr%fEb6wo-9S1PI88s2o9x0RXS=_K(NS3f%Uz%}ZkXecxyWM--41(T>qZ*(`TD6+~MhMQm2(MRH{u6f!U@-N&2f+I8Buq8F_zbN7Tc2W5 zH7UI5R_m1vl0H9Ms5kkhK=%>rNy@&*$p;Lrcd(2ww+Lf{n^#9oi=(w=w>0^WsW_~6 zj`P7M2LFKiix2=Fz6;4l(~x5I;qZ}WSNBe9JeV|%-f_W7d_4qO@KfRNH&Rhi@qHCM z)A9bj&dyfR1UdLdc=m?tDWsyS?OYOkaz1*yU2e)IqmobT6bn|~5COvD|LTmNsX+52 zqU0b&w4ggeN(ITBH`Iez=h1Ln749vN)v+C{`%>*##8 zbLL*_s9W%lf~0X{*kFaP2N4@J-BP_r`jaRf3)gf^-BE{ z(EZVsn8hpqQ6zu$iWn^(pv503tD63we4Bw-G{DyVNm&>QaHafLAAcS`0};X2v_fz0 z=&=7wfB$&^2A4R6<(*3!^M4u)QmBC9zk^PUf3Wnw`uoHIc!B>P{~#&0z-xSq91-Ke z#0ry-i+vP%()9m~w;;7MSN!n4Ob+4%g{B}zq{khy2aH$uIo?FSXBH7~8J8Wv>pz)T z@eE8$sn+9kt;ckF4Y8e7cNuIV5XM{0THzdyZVOZJ@IC9zyHc)AtJmi>UR#S4ri34R zt=@i^>%SP7b&lToXHLbu@kuyL`boex^kLQ`n`bOb^_Mqqz6@veRka_^+)xkrZ+a{+ zINlr+SbZylzHP5hO56z-y9NJg^tkccTE7y*cC6|AHE1n-3b=bZWxrb+NWyzQQ zF;y_FkzhpWY_$5_6rcRE)~8VF+0 z{^DX*BPa86K?jgQHzLmkUT3Vv?47-mMRt*46sLuv3LI!{onh|FTSX{ldu_Et{r)Y^ z!+~Xs`PtHw!_%^){VgE+;Ezncey(ERYk76K9+pmSw{4?mmf+He-;mcEBf^W4Fewj< z5g|I7`_-?-GYfJ0xEt^|LhSpU4Bx3mEF08|ZzDP;dr273*;cu1XMYT+fv6_p{ehI`m}e$1Quj0%SA&Cru)kR@8)Q;{^U#Yjbk=)e zgBwA7)+pe8&cU0pPua~?#$FQdHBJ>qBT*9cm_?wDP@l=)}x%m#|-nW-&n-Ve>n zf_3)_J-FyUsZ>#4L0LnLJj}%1Mf7I6v_ZLXH8r%NB9-4Z|Kc2GZOm_3&8``Dm~T>C zpZV3N<;`yDr)mVt-VX=af4t(_-`x1(UZM}qsxs>dLf=hUvsvD@q*6qCio=(lT@R3- zpv~Xn38Zv;nu-J&E4-1R3>am0jN07rae&Y<1mtg|fim4>Hm%{94` zw-2CM=s62?_hl699(`Ex5L(D(ISZA@aQYryAN^yqSzdp2^d_YOUB; z@}ERXKn}#b@hmfJe`n$4R<;npZu*?&BPpN&<;9K_dG?M+Thxf7`eCCz(VE%P$B%0r zG>t1|ao~~^0$S*QqbA>bS|vnW(ql}e27Ph>MYDh$cIO1KeDAsp$PNE zPZKU4?7yJ*oc6wj0|-BMT+0o4{9@S<2i-;6akbqGsoYSeb8dgY$(Z}?%iH0FR_9v- zq3o2DGcILaWSI&}M|eez2aBk`guut;vqL8WNS#8SV7ah}SAU8=7=f87SkE`?0e0A5 zY_%3)Nw2?t#5Gj$bwM5>t;Anpd=li3u+(pue%9zmi?&?K%De$^nE%c}p~)8s{uD_i zV7R$??&Y@ZQRu7Jt5RuwE9$s6a37q>b^nT6Pjk4z^x;%?%w5Xq-Au`&wQgJaheegy zl&-qF+3TF#Hjs9#O6q+efH8D4_~L@RjjK;v;uMt+N!IrD6XLH0o=)sv``J2OHH)}k zCv!inN9ZqZz4Sj5$i|I6yuN2-v|4o7|xIcEN z-vSkVffoWCf1`hfl|OIbLV$kUqfkk~of=@^eeC+Hyw_mm5;A`^=OF{C^ob12&uyVyCT?grT7rT780rFttG5K3i;rXM>qMEN{b zlG06vnD}%AFT}79On&N(fk<6CacJ1Yvm-(EBL2R3&qowEX+AP(!&Wn)Ih(baU8S0@ z6;4KEz`%BX+Q^B1Z-eIb_*3zTUxT?_4mkYCYunl-#L;M=-m&h|97jTGx zZzs0y!dd{ZOdOf8`0BE8->*w?7;JjYR~*8*9FEs&mNAYmRD1&-3%YftzBIxS+4RbE zIBHpfd-6Jxmn$Qlt9av(2w(Jfa_+mxWOF}~IFn$kb|5O#QKR(Prxh`sVRbcvYSW$5 z&W`2%WBiP=q2;=<{d{WO(8{UC{@9wMH=OGX-Phr0tVxhBTk^K=7s0d92i0f7>b1$r zzo7Je#~Ns}p93JJ^)w3Hgjr}D6yCi7V`}@>0++M1vjuKtNGzD*tYLkrH&Sg)+n2C+ zgc(oqY-Cg~Cs-K7_kAUcubzNaC(_C>QacJeg?`ih6R8JV_l98asy;^EG4`aV``peM zF*l^l#n0AcfgLyiA{*Qp4xBR0vcXHm_6zm4nSAx@^2v!4^@%?|c2}+4>%zw(ptsEw zqpnPNZ;YXh^(ez}5HAJ~!#psxYrx{IG3p?O$?)^%9Pg=ZW}KeTS0?X~cgH(`V;uA> zN?NRu4!>9%z9+NJ#5b2*YRR^gl)yZe9Dey7PhLUv&Y(Q6ZxVuFR8x$41vmcsS8&<1 z#kngeMdUN^Qt44^skzhYCTP$?js9f5J{d$R{`mn48lKp_z|Rl5nV3btlFTJF)$ijk zlzw*dPj0fMMKniTf}l=|JC_?Rbtlm|2p>eylM!4-%Hb*M@Niq-z~kvgjD~9GHj`c6 zq+G9v3n~rUQ=yTL5@c#O)DPPnkHiKgeyWPd%paX*`Pj?*#-u<#(7woKJ};P=lAX;{ zdm!k8#)J3o!u6aF1h8Xd$;b)Xif+9vV@^DH6<<1a6s$w7mUs)NTnaGcykhIKihWC? zQW8%pY|aYIr-_7-c1cbP6YKyQ!&0No=_2`xA4`Z0J)6iP{pABM)a0nA z=BAT>`M;WhwRwXlV7vuTjS|auSFa3rKcva1h2`qEpW#ED{B6n4zMa-cT;q7r=+u!Y zU{MF$LoIkZ3KVL>mc*vqAc&~C51DfB7!9JER6 zc!0xj?&Dbz9tS7Wp~2?-?fa)am}vB3%jHQ+9%H?Ygv^?jw!4-}_4eZA9o|#7mXJ8z z=Z>U-Ku~veirMYB zRPWznZvN?e>{`8t;T6wF2Bpf>?1pC5Y`IEY#`*N<`PH`0myCuw4IoZt>#dNv!-7uGHN9{Y%upKjlu+fG4rX=x;Uu_uKy;pOfwyc5`6iGs8Q{92$FK z!6y0T`LhbyLfZ;%__KCiUQaffSj&3bMbA4MvPLyO2GRT#4nRWILhk*tHq6Ap#wSF{ zDC|9EVtdI$=S4XA{rL5MuY%l(g~Wg4df66`r4h}zJ^VhrO4ec?S>G-jBP(;Qp}}+d z%NlS@0~wgNx1HLs19BOEkTR&o6xD$e$?%@$%MD57TLaw(`PxzWlDhG^#qsj zYQP%+^H$L}PdOA&>tPj&ns#3W{P%Y`2yaRUypE@x)O7e~7>&&Sj>32l9ca(##E^-N zDR)u6JsiABpClLE#2_d|{Row|P&ymEoiKDO0du-TdNS(Fbl^`%49elD)mX{LIoNM?@cL+gi5C)h z_LOcGl}DRW#HX8F$hF&NbBU4z$dG*1;tqcpl(;whp=EAEo;`=!2trjq1#K@jAK_q6 z6X=Q9Ez zi3BYBs+qIjC@#n6PKe)u_r+n>T3QlW$w#?!;}6E4)D^n9fzq+*inFp3hCDa3P37d} zgOW9Mb-K=6WWvE(0Utm_^P*Vic>oOIZQaBt|)hD3D#Ta|T8@;hmyrb4q#S+dAkGBKR@ z?<{TJM5ZQUuw@n-%J;nK5r1dt&A#sS>FaUGa(VeawT|%D&3N#9suye9*d%0-cM}Rh zV=cQ_mVJiTM+_{P4?jU7%{Z#t|;c_y)I1uN^zUjvtC zr>E(Lb>-Q-H{=szV`laH(^fIt)}WMT^%(&|!X8;|#L=KCBQUsJN3>n^_j3eWJvW#` zhIPemMN{J%Pqe=VqS*P=nX;|*lmUzjc0YF*os=%JWq0axKSI9|ElZLPR!Y#2k34S1 zba~$B=;v0ny$yhavO_lzh?o@6rUe3*FKmRgv8QJVo}rZ$!yC7U%prq_pp~t?W$X)q znIDO}uuxd}*ry)wAcebckFC{$dA0T}aa7RLLF?(x50{W*yMFI;JkNP!i#Nwb%!js3cP3= zP~K_XqSo{K4gD7)=>Gn`acxf!U%2{{Ka0tZzKgyB?8hK2rnFa_(MaQ*E&Utt{?NPEIP)SAr5%Y}nqU(64Q6Y7LL-Fw06VboX1kzpH#Eq6WluJJ0!o8gIU$ zc(ZU;JYsk1-5gz!J`Hl+2D)TxPKRAwKj`+<_-g0fSFg1`PEd=SnO>IJUnQpSNlc>Q zdu_el6xJ2yP-4GaG>WQ8vf=4FPzmeEoYd6M=?|`BJ;dqgh>i3OaQ36$+rZ3CJ|TZ- zPrx)VLF+A{#DpnMElKTvMP>dpUI$HrHFqFCyZL$xxhz7+V2!LzFBWzL)|x~>@|1p|%U}?}!Ia{!cH0Qj-q+l|KgZXVp$u)A?td*O=jX<&sx25d zu*p>``V#J``ohrdph{L4El&CCH^LQ9!oTyE|9E7Vl?n)~azL_%-;@6sToTO2dRlw_ zZsebgSw3+X5NhgZ?B>qbSHzb+Bmi{cQ(#M3{UHArXamIFqF=_h9}tR^u0PIsP51a4 zpa4lmNX?}Gqv(LA7i?nHq=y6wC;#c^P8DzfcprLLs%rjs*#G>I`Zpj`_@Y15asO35 ze@rv1|84qzC*-e@{{M!Auq&gHgGsI;`k&TaO0Bo5;QlM4)Dm~%DUha4=Z{WK=YG0= zMenihAggTficplX!Qc_fsI z8F}~iDDuCR4A82*oLHDJebUyJ-&kn*BEk^@-cqDjRDSVZKtMpzTPEx!+sju8^ctz! zEN+l1bIiZUm01+fh6fZyvW8_}w!$};> za0yi9YXy*U2sRhgbFB%b}Su^fnqPB)8KeUoo}`p89}wY0?lC^yY-pMPW4#?U5TT}d;J zb$CE`nn)v~@b*eFHWjEGlac-5r@F?_YlOuJ55in)+uBXmFgZiwkMepN8epf|c5p4w zGyZbpV9cNQ3YYS=4W;+^U?%4ROZ(c-);*NOckCt6%Ayvh`*RMn_4^!{kI8vzN{R>R zhz;;+pAh%Xd`rg4Gz|D~ax7gM*s1d{F)67SR5#!HVdv=rVmpIE&`4!`*6cBrh>3n( z>uK`FW|I0`x^)W@AQ+jmoogR>+7qGBGwIA_7oGW%_@Ung!MQaC0wdvra)>cd1{eptyh1+P_6vC zcYGDdMB-kc2ul1NAgA6#nNuusKKkG?aRS{bhCi=UzwNc1tD2ZU$B3ay?;7_U*`a;(pfr!os&hDuEO4&hq7;148zeN%bM2 zkxBKDJ$4(Vhy6eB|GTk!__YyrOo*;5K|xk0oFB4iUvEE0sV|=O%FKH7XF5!5)*mAg z;L9wb_z@tdA*VDjFfi2GqyZeLbnlBLo>uff`nyq@o>l_sHgoT+i#IW=cBT(yqBbvn z*s;)&;yL!O5fFcL$t<1iB<cPpVtqP< zRjgWfdNXHMIaf?!!7E>(=n?An)e@*?d$O-{&V8SAffT*6rGkUQ9s}@sH=t2fy+g3_7Km(X?}e_%$b5sAo4jWC*I- zQ_NS8Fl9&6G0A?)Q_j}mCRUhMV~+MZ{`2vg(NKHI?L60sul<)FcA2-*jTA@87T_)+h@9ah%Z0 zb6`_e%<0>1>cz$!%xf}MR&UB z>4K25gC3-Aj@IjIqlw-u*t2D{pw*3nXy1R%*~X>l*`B8t%aqFAR6FX?d*vy=kpPNIGwB^s6Bpo8j~AR zhsR-?W%t#3DGiTN3OGPW2*D;j=g}yAmgho}zr|$G3I4TwsMVG?MH|CNG9LUCLPe+U zyeE(D7+>__zwD+v0}zh~XrMBSvp@2WIr#6}(*WQ{qGWz3{`BuBf4om06$3tS+gd~& zz4$M8!`8nA(0#4YSviTAJ^W$kW0J7bB8QK5G z_*CuzBJ}^;VN+AqqpBIA9U0`BA09PaDD9j>l0nF9xY>|qJO9HJ2w~4^#aY5aNrT<( z&7v)=^3fk_H87lrx?QQ34=hQ22FzZQC}tux81*3$4-X$_4T~P9PsJHnk5{;gea^j$ z&E=Y1kiK4WDMzEa*?6oP+GxLfBfIMFk(3Kd@5YIn3I=51HcVBw7M4zw6F9NE4Z z*&~A>IomRRYhQE6IJNHx=Gp0<#uQs8)ffUTas5R!*Z zyWBlLA`o*CX{&L%@HI>KHqv$}`s`H_Y)YWFj?9dGD+=bX`Gp*onX~%IJf8_K_F668&}YU`mkuXf>Zbrf5%0 zEXdoPUd>!b5BzY6zsH*9(pq<6J&ACAnCkM01`^iRR`kY>HH$7>y?U7UmSXmo1|q^@ z5~rc2ieuUp3_^oE%Rs6$=0gt~!&k+-7-`Uv`POF+4zS_&G=+}tu7091@ z{n}4k%dewipfgN(sihu@$iDy!=I2NG*X^{+wk0ic>KlNYS-L2|Yz*z$a$)cz4d7ts zBg~a(^BXGgBj;i z=&NtX_ttYaP*?i5Tu&_O29TdJYRxi6Yo6>@ zi>p!$(}ez0^npJ|uCH{ z<+>_2&+`2sVRO3aaER|$-bT(dW;quJ%Z2&Y;c|xp^hmaxl}0)v$ZpBxx@G>8QO&v- zzp^s3y?)~masO#ejU`U1@!vEu0t7}R$Djk%@SSYifr)`oW_gh01k9w}B@BL!6GKIt zJtooOEp%}ZF!8a z*ZMxfXFtsnnC0i$i)9k+va$;HC+#9mM6Kj0FJ;KMCIo5P^XWWdi-FsSZ(im_&{XWn zNtU$Vg8PzkmHKTWU!eX_nAXmzi@cTWHj>hZgYNTGjM{fi)dCsuja6(Ovg78a>dCsZ z9CHP1v3Ae8=C)g@IiYW~+GmOB_ANoH&6`^zOmyjL6J*oQd{w_ii*2S(V&E00(Pw9J z8$6b66)f7*$4AFrX>c`p6JqNQGr43c{@V3$)3l{%R}mW>Qc?#eUqoe)MVO&K?Abzz zY33XI`O>m-hgP5E_8ZTmX5C)TxK$8@m+KOf7TFtn7N z&XnfQ8j&mw=xEvMJqE5`0DJk60L#UvjwwRT-+U=S7D00*GW$}`CyUF%O2PkF6U zu>3mFx}3S$Yykm>_Y+?!faO*4uQe&#!M#?_eyoBHkRn)XJw2YZL+~(059=CFcAqvH9sgRS8#po8yAH^B{UAem5{ZHcUKfgE|0% z)M7Y2so=&@T+(9k&1j=;1@;HQwke*`#FJJTRRC=mvo&^HrBSWdTeM^$xZ~^uF8=Q{ z>b?uL)rg1hg!kM^zbXEDLCl+#VmQ)%)jpH@xey~VacIHRf*k5!=L^sAsbL1Wf-$c| zb4;Wk{DyIVf=BKgU3Cpk*&emnATA}-2F4NCCN+Dl@g2_%k|tnLr@nHYV;Zxot|Pe8 zo{yerwBRPak7TN_|7ET+V4WkoaZ^24ZwJ%lu!s0Dm6WjWIS5nQ>1b2^F;F|uj~#Ac z6II+d<^ESl0fU{&UTFK#RShAc<*csu*l1S?3R74DpEi=hUw>Jqq+K|@-gnG6j51#a`DF-;p<7+(gm-Z7 zWXU^@ss|ACQ`llE*ExKARHpO$^`@f!cD@tFquShV`wGQ*B}(&Ye!Cs0K>Ob595kMP zH~pDWpBiqS<_>J$-U?PrkI8#xPzVk|_b{ox(sG;=?=ntBH&b&kp=?ec)*Ez1 zW0a0Puwg28MSe$aHoLEhx>6NAk@l|0FCwvel6rLWeXl=v&s#>L^0?payYW)uPU^SW zMP>dnai%ik^u?)}b{La=T2M`!mdsg#m$d<<5dRehU2(A1TYm@};6((K_*a%(K2O>$ z&xEr0=P?!HAfEyatzh;~V0|)Esu9f|RkoTAro=7x5Z~tYO;~Wl)Eh&= zedI>I;xzNdnjrAYiUWkY9i_;spZtMuOSGTlMy%EG{orHofM$O1tr8XZh0YWOkJ~+o zg0FUxh~h)O&)v{N6|N_X zvVxFJLA#-WPy|Bg0fV$a zFoqBykUU46d9K&@FL=J3>pEX{&e>=0weIy>_myp;-);yFXtDV)cQkJ>!on?GR+@q# z>u4o8jI}{f9~MQH5boSD)J;OqPy?lsz>XZdr!6nLz}r(jmr#%{(TKvxJBZ6sk{##v zbM}wy8e{I-i5M{;pUm2|k%g!9f=Rj_4|bH#0ROdVGvU4oY#U3woJZNSqzttfzO_=| z)!fNx8)w-2=3=c`{AKLeR*cyt+RasK4*x5()KD^7x+(NB7Wp~~_CW%7Zwx;Cxs&1( zwEik*b})XEup`F%riOBY)k5d#eL>kLK-Zy!o` zXAo@Q5%Qm3CPUHAhSi-LqG=u`j?Nq#^b9!mWh)^>9{sx(h4+NZ!n3-@x<4yJ}NnhC>&JX;ntj zQrrA1@jZ(ljzHw?E;w7qn3ReRA)Vx>bI&Tm-xaHHGOy3yx;u$(N%Ea`;(FOC!7K6O#D~c zm+08G{Ch;vUnsqat=H>H_dM#u<@;pR)4L%t1j5=b>Kj-pf8UU&y;fY*CTsOKQKizk zy?DwH;iZ8)8u8ce`2y=}uSo|%wyUFwUy(Gcz zFYK86>(7TPCcxc<_{S)XRe@`lSjOj4aL#brWoepowaekHBpEXCr0d9#>H`|Ac2xe4 zpB+db2|Z@SR-f9{RGH8VX%`dG0bkQej$`9L4;obhXsdQQ}w82t$>T03oxs)FOg1HqS)ZONuj_%-u|6ZOP0y=1& zK_%ipB7r6V2^2$(D-QgC1P%!U$l_|D#sARI={7F{r?YcaZ{2EjbthP06+-@@P91e1 zC{zK>{y=T^wsY97qkj!s|GH4o^UKcYkE z(kAC`PWQKjQh`Mox0G5Fg|G5!ovB`XOZ+}VGstAzi*}~0=wiWS>2U*9cV@7pwrBCL zUzbPP4GRLIEz5efA8&w_X7Sw6wYQjkC;F638{XT3K>Ffm(z+P8WjNCH&$!4D@uB`d z8muyCH7d7h;JH^1WPrav{w9k1gh>%Dqf-Wqq#9ac)oD3?)R0Igsmp`XY)FTxm(ZFDpP>1!oHTlR*7&asi)cjUHr9*)!04So zS_o>_;7Czd*EKkzM3u7!scz|7?W1w}`OozGw~Nf4j0mRO68@5_@NH@RYHgaLRb;7$ zd~}qHD3u&CoFlnzcGP&%aW>!#p+~vuFd4oW3RXeW%O3*FsSlaxTWxu^GCpkxPo))PwR2N*f5C=LjBokR#*@B(=igUA zX@2VKHF!%V`;w?+F8(Ing2f)lzN5v2z9E2nU}>MKjzDsQ@FH({sfwRbq}X(zF4k#o z=-hr8Raix?3NG@66+~!q`Gq|(KcbVlOQu*+l<~C!5Jw1t_2hU`-^)AM_ zYYTkY{kYa!|5jzttyTiN`>jZkZ2UIrCP^2~|8E)9x0q=DeZFgj5+`2gUsYUjlmz?; z<&zpH#%<4WS-8~oM0Dy2gUj_wM>8R`b>41ENB-r6@Y}xi_xH-19eerVoK;gz)FdNr z)=S|d0>y?cu)@&w(as0GkiGj(MA|2ZcPOboyL4hN2;LX=YQk{!vO*!(qifu_u_kCU zQm7srm>tJ!tRXSx*{!))boGWK0255GXf3?`82@65ZLzc(0yWWI7WnSS^45wr?z0P) zGC0(*Hh@wMnJg(`2g*W&XEj0vC>Z{XUSQiAy9FgZ1djj)2{WpgRL-iCB&4${NbI~F zaAJC5MJSOvg1)nTlF-##Q9Wm)s=PTI86XF|CHIW#S-yPB@x&tW>4Rs)@a{$h;ksRh z(u3`rTT^~VY#&i8*W>Np$0@t%)-*Rl{aAQR_>FNL9)95>8|`sOPK9_=&<0SHMNQsf zDTTsI)Yj$+W}EFfjcOHjd1H-2k5G5>=L;=qxZo}#`o5;f(pilI7TPf$tUb7Kq$6gW z(aYLik%kIP_`TPYArqcsZbQ6tZKQTCP_qmri+2uNAD;z`VN*rq{rdwC|6OWIC!#q+fGD~F4yK~i znw4$K(v_@AR~DC)s2m-n$Ea@Z5*zvO*w{TcypGE!Ll%@U^ty;onu3xW@|!}+l!PW& z%dH_>hJy#*V~Bw^noYeTjgdYY9I}7I`OKXHQzS*zJ-%?$Ro>y=9Yh{|%y|g5UybwX zQ%GICCPcM3%!1o94(``{H{4hDf~75K?!ja)kf7+KAnMI4Q8_@fq(K<)Ogi}d1ZqBf zb)$%5BCEUhJM($(*^h%p#k-ZDWK^BiU}*(xN|&gJNEHy+V|vZp%u%YM;!NqTC!cgo z(7R79cnX*CRy{m%M%^Xb*=K#w#g{IW+#_dP0>jpxyb^Qr5#CYjdV+&aocmH;Zyt*> zG5G{5zUwaAlPfb?-zK`Bcyx5OiRn}|I*2ysR4`Z>y8g*ZkXq2b&^^sz`qXmkq4iUxgp8!1x`^fl86qYFPs7nkZ6nZ^hkQBCB*r# zUNho{jm>aCVN;&g4r=*P^1~XH&-fdo(E-awyTP-7J>pwZ1&%cGnosJ4`i7*RZE-jp zVfs=ZcrJw)v$#xNVY#bx4*Cw&ap`D)ImJ7VQKvB$`b#y}4O~j00V~Kt(VKr)1L&a0 z36~VkTnUZR@?8l_ef0Ux!>-UKG8Q|J+jXqKw}#L2-p&%X5R4UP(A+3sY`HYLmA?`X z-EL5BV&O^Suzp?J`Nn0b#OT@Ev)_%a57IgSeBEtnk0`Tdxp@}0={0&4Kdg;4RCtjY zWHLAZB%=1+uix4ICc=f^ zCB(i?h04x7PX*4~9H8~Sk{(F1Jjb_Gt;0L3mvIMWMnAVO*lwIX?1MZX`fQ-|k?Uic zAw?8}#UXlYJ;rBa`@OsTjf6!7kv^#dA4f`^da(47YpL4k1^7tBV$pLBWl)`>!^0ZU zmSZyv-9LC$g^6LqTv#95(Hj!q^JJvRnefS)&suY$~JhX z%tUT@SQ2gSg3*8zcPn|#1ndcyu=MAaGqR$u1d$Mre81Qp11K$#Q^@ZRGYMA#FxW_1 zxJeX^k)ssq0W`)^%s|^H)zlmoV1gvNnqDtzSs8rCI}xcKg%iUQB;Q0CLA4F^E2KZ- zvae27NDe5(|2m#z>A1|ASe=*ZH73r6DN(d$T{2-$#fOn}F8zo(qNHO>SEOX+BRvE| z*7mX=q8_k(JCC^f$hZcYI|}u)2~DcYU`iYHA}&PZ|4nbTpd_Z#M-uaX=P>3#2*1Q- zU6^3_IhV&E8Kf(=2m4c%3bU{muLW7A#FvUwR+N3^0mEnR0w@B@|M_Y z4v=!muGf$U%{|4;xFzhO)RwF2*tE=T?$g;-oz_#y9l|NSwH}}8(0zvl4?T-ac~()S ztk1T~J@R~!JHpQG%}bbWsB}R2;TsR0hD0*6S(-!9m6bVJHxuQ~ySII5$YV+Bc-ng= zEhNv?i@7{JxNoMp!&4d0e3=oY>|8yMD=_T+kf2Ctgo=JzpQu_xzFP}THFXrS>3p5O z)_qbN-6ncJtm&tqM05hmby!bbv&v`m_Nf0AW#!|$h{=Go$eSZ=jL7&+HTOk{mSQ; zj+Xjhr$oOAw4K%W&xPm;$jQQ#5u+bea&pH!;DNXjL6rPKQwxA-DnRV%=~$BczheMe zZ53!073=!_zu{A&B=t0;>L75+dnbteoL_yy_n%j^_x@@wr;^+CRaWz%UeO25j{2QV zPW0+fH+fQRpb&qE%q8*!mi~3`65UNT#*HaY^o%F_mUeiWH%LfELbnJpASn0b(hfs7 zasE$G6}WayaWz>Y?z}rT*K0%M)4E`O^fz1M5AXaxahvkj;dMSN{0TXp?GNM<0fB3m LZOm&fdf)vI=f3ra literal 0 HcmV?d00001 diff --git a/new-member/README.md b/new-member/README.md index 0179204b..15412c02 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -1,11 +1,15 @@ -# Part 5: Adding a new member to an existing channel +# Part 5: Adding a new member to a Fabric network on Amazon Managed Blockchain Part 5 will help you to add a new member running in a different AWS Account to the Fabric network you created in [Part 1](../ngo-fabric/README.md). After adding the new member you will create a peer node for the member and join the peer node to the channel created in [Part 1](../ngo-fabric/README.md). The new peer node will receive the blocks that exist -on the next channel and will build its own copy of the ledger. We will also configure the Fabric network so the new +on the new channel and will build its own copy of the ledger. We will also configure the Fabric network so the new member can take part in endorsing transactions. -Adding a new member to an existing Fabric network involves a number of steps. The new member is located in a different AWS account, and the steps therefore involve cooperation between administrators for the Fabric member in the existing creator account (let’s call this Account A), and the new account (let’s call this Account B). The steps look as follows: +The architecture of a multi-member Fabric network on Amazon Managed Blockchain is shown in the figure below. + +![Multi-member Fabric network on Amazon Managed Blockchain](images/MultimemberNetwork.png "Multi-member Fabric network on Amazon Managed Blockchain") + +Adding a new member to an existing Fabric network involves a number of steps. The new member will be located in a different AWS account, and the steps therefore involve cooperation between Fabric administrators in the existing account (let’s call this Account A, where the Fabric network was originally created), and the new account (let’s call this Account B, where the new member will be created). The steps look as follows: 1. Account A invites Account B to join the Fabric network 2. Account B creates a member in the Fabric network @@ -13,9 +17,9 @@ Adding a new member to an existing Fabric network involves a number of steps. Th 4. Account B creates a Fabric client node 5. Account B prepares the Fabric client node and enrolls an identity 6. Account B shares the public keys for its member with Account A -7. Account A creates an MSP for the new Account A member -8. Account A updates the configtx.yaml configuration with the MSP for Account B -9. Account A updates the channel configuration with the MSP for Account B +7. Account A creates an MSP folder for the new Account B member +8. Account A creates a configtx.yaml which includes the new Account B member +9. Account A generates a channel configuration 10. Endorsing peers sign the new channel configuration 11. Account A updates the channel with the new configuration 12. Account A shares the genesis block for the channel with Account B @@ -26,6 +30,21 @@ Adding a new member to an existing Fabric network involves a number of steps. Th 17. Account A updates the endorsement policy for the chaincode on the channel 18. Account B installs the latest version of the chaincode +## Two scenarios involving new members +There are two potential scenarios when adding new members to an existing Fabric network: + +### The new member joins an existing channel +In this case, the member needs to be added to the channel configuration for an existing channel. To do this, follow all of the steps above. + +### A new channel is created with both new members and existing members +In this case a new channel configuration can be created which contains both new and existing members. This configuration can be used to create a new channel. To do this, follow these steps from those listed above: + +* Steps 1-9 +* Step 12b +* Steps 13-16 + +This scenario is simpler as it does not involve updating the existing channel configuration. + ## Pre-requisites - Account A, the network creator There are multiple parts to the workshop. Before starting on Part 5, a network creator should haved completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. @@ -95,17 +114,17 @@ aws configure add-model --service-model file://service-2.json --service-name man ## Step 1: Account A invites Account B to join the Fabric network In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain -The admin user for Account A invites another AWS account to join the Fabric network. In the Amazon Managed Blockchain console, select your network and click the ‘Invite account’ button. Enter the 12-digit AWS account number. You should see a confirmation message indicating your invitation has been sent successfully. +The Fabric administrator for Account A invites another AWS account to join the Fabric network. In the Managed Blockchain console, select your Fabric network and click the ‘Invite account’ button. Enter the 12-digit AWS account number of the account you wish to invite. You should see a confirmation message indicating your invitation has been sent successfully. ## Step 2: Account B creates a member in the Fabric network In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain -The admin user for Account B can view the invitation in the Amazon Managed Blockchain console. Clicking on the network name will show the details of the network that Account B has been invited to join. Click ‘Create member’ to create a member in the network, entering a unique member name and an administrator username and password for the member. Note down the admin username and password. +A user logged in to Account B can view the invitation in the Managed Blockchain console. Clicking on the network name will show the details of the network that Account B has been invited to join. Click ‘Create member’ to create a member in the network, entering a unique member name and an administrator username and password for the member. Make a note of the administrator username and password as you will need them later. ## Step 3: Account B creates a peer node In the Amazon Managed Blockchain Console: https://console.aws.amazon.com/managedblockchain -Once the Fabric network and member for Account B have an ACTIVE status, it’s time to create a Fabric peer node. Each member on a network creates their own peer nodes, so select the member you created above and click the link to create a peer node. Select an instance type, the amount of storage for that node, and create the peer node. +Once the Fabric network and member for Account B have an ACTIVE status, it’s time to create a Fabric peer node. Each member on a network creates their own peer nodes, so select the member you created above and click the link to create a peer node. Select an instance type, the amount of storage for the node, and create the peer node. ## Step 4: Account B creates a Fabric client node These steps are identical to those performed by Account A when the Fabric network was originally created. See Step 3 in [Part 1:](../ngo-fabric/README.md). The steps have been replicated below. @@ -262,7 +281,7 @@ cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsToS3 ``` -## Step 7: Account A creates an MSP for the new Account A member +## Step 7: Account A creates an MSP folder for the new Account B member On the Fabric client node in Account A. Account A stores the certificates provided by Account B on its Fabric client node. @@ -282,7 +301,7 @@ cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsFromS3 ``` -## Step 8: Account A updates the configtx.yaml configuration with the MSP for Account B +## Step 8: Account A creates a configtx.yaml which includes the new Account B member On the Fabric client node in Account A. The configtx.yaml file contains details of the organisations in a Fabric network as well as channel configuration profiles that can be used when creating new channels. The channel creator originally created this file just before creating the channel. The channel creator now needs to add the new member to this file. @@ -300,7 +319,7 @@ You will make two changes to the file. A working example of the updated configtx Save the file. -Important +### Important This file is sensitive and must be indented properly. Artifacts from pasting can cause the file to fail with marshalling errors. We recommend using emacs to edit it. You can also use VI, but before making any changes in VI, enter `:set paste`, press i to enter insert mode, paste the contents, press escape, and then enter `:set nopaste` before saving. @@ -447,12 +466,12 @@ Profiles: - *Org2 ``` -## Step 9: Account A updates the channel configuration with the MSP for Account B +## Step 9: Account A generates a channel configuration On the Fabric client node in Account A. This step generates a new channel configuration block that includes the new member owned by Account B. A configuration block is similar to the genesis block, defining the members and policies for a channel. In fact, you can consider a configuration block to be the genesis block plus the delta of configuration changes that have occurred since the channel was created. -> For interest, 'genesis block' appears in two places in Fabric: +For interest, 'genesis block' appears in two places in Fabric: > 1) The orderer is bootstrapped using a genesis block, which is used to create the orderer system channel. The genesis block is created using this command: `configtxgen -outputBlock`, and is passed to the orderer on startup, usually via an ENV variable or parameter (named `General.GenesisFile`). The system channel name defaults to 'testchainid' unless you override it. > 2) Application channels are created using a channel configuration block. The first of these becomes the genesis block for the channel. This is created using this command: `configtxgen -outputCreateChannelTx`. @@ -661,10 +680,23 @@ cd ~/non-profit-blockchain ls -l /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer/mychannel.block ``` +## Step 12b: Account B obtains the Genesis Block directly from the channel +On the Fabric client node in Account B. + +This step is only executed when a new channel is being created with both new members and existing members. It is not to be executed if you joining the new member to an existing channel. + +Since the member in Account B is already part of the new channel config, it can directly get the channel configuration block from the channel itself. + +``` +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer channel fetch oldest $CHANNEL.block -c $CHANNEL -o $ORDERER --cafile $CAFILE --tls +``` + ## Step 13: Account B starts its peer node and joins the channel On the Fabric client node in Account B. -The next step is to join the peer node to the channel. After the peer successfully joins the channel it will start receiving blocks of transactions and build its own copy of the ledger, creating the blockchain and populating the world state key-value store. +The next step is to join the peer node belonging to the new member to the channel. After the peer successfully joins the channel it will start receiving blocks of transactions and build its own copy of the ledger, creating the blockchain and populating the world state key-value store. Join peer to Fabric channel. @@ -747,7 +779,7 @@ You should see: However, if you query the balance again you will find the balance has not changed. Any idea why? -Your Account B peer node is now a committer peer, which means the peer is able to validate the blocks it receives and maintain its own ledger. However, it is not yet an endorsing peer and cannot take part in endorsing transactions. The `docker exec` statement we used above sends the transaction to the Account B peer node for endorsement. The Account B peer node will follow the standard Fabric process of simulating the transaction and endorsing it. The endorsed transaction is then sent to the ordering service, which groups transactions into blocks and sends to the peer nodes. Each peer node will then validate the transactions in the block. The transaction endorsed by Account B will fail the validation step as it does not meet the endorsement policy of the chaincode on this channel. The transaction will be written to the blockchain on each peer as an 'invalid' transaction, while the world state will not be updated. +Your Account B peer node is now a committer peer, which means the peer is able to validate the blocks it receives from the Ordering Service and maintain its own ledger. However, it is not yet an endorsing peer and cannot take part in endorsing transactions. The `docker exec` statement we used above sends the transaction to the Account B peer node for endorsement. The Account B peer node will follow the standard Fabric process of simulating the transaction and endorsing it. The endorsed transaction is then sent to the ordering service, which groups transactions into blocks and sends to the peer nodes. Each peer node will then validate the transactions in the block. The transaction endorsed by Account B will fail the validation step as it does not meet the endorsement policy of the chaincode on this channel. The transaction will be written to the blockchain on each peer as an 'invalid' transaction, while the world state will not be updated. You can check the block height on the channel to confirm that the transactions you invoke do result in new blocks, even though there is no update to the world state. Run this before and after you run the `peer chaincode invoke`. @@ -789,7 +821,7 @@ You should see: 2018-12-21 04:38:18.130 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response: ``` -Then upgrade the new chaincode on the channel. This is very similar to instantiating chaincode, and should take around 30 seconds to complete as Fabric creates a new Docker chaincode container, installs the chaincode, and calls the 'init' function. Keep this in mind when you design your own chaincode: the 'init' function is going to run each time the chaincode is upgraded, so don't make the mistake made in the fabric-samples where the state is initialised in the init function. Best practice for Fabric is to have a separate function to initialise the state, and only call this once, during chaincode instantiation. +Then upgrade the new chaincode on the channel. Upgrading chaincode is very similar to instantiating, and should take around 30 seconds to complete as Fabric creates a new Docker chaincode container, installs the chaincode, and calls the 'init' function. Keep this in mind when you design your own chaincode: the 'init' function is going to run each time the chaincode is upgraded, so don't make the mistake you see in the fabric-samples where the state is initialised in the ‘init’ function. This results in the current world state being reset to the values defined in the ‘init’ function each time the chaincode is upgraded. Best practice for Fabric is to have a separate function to initialise the state, and only call this once, during chaincode instantiation. Before running this, change the members in your endorsement policy to match your own member IDs. You can find the member IDs for both Account A and Account B in the Amazon Managed Blockchain console, or you can look in Step 7 above where you added both member IDs to configtx.yaml, in the section `Organizations->Name`. @@ -850,3 +882,4 @@ The workshop instructions can be found in the README files in parts 1-4: * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. +* [Part 5:](../new-member/README.md) Add a new member to the network. diff --git a/ngo-chaincode/README.md b/ngo-chaincode/README.md index 845b797d..a96eeed9 100644 --- a/ngo-chaincode/README.md +++ b/ngo-chaincode/README.md @@ -169,3 +169,4 @@ The workshop instructions can be found in the README files in parts 1-4: * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. +* [Part 5:](../new-member/README.md) Add a new member to the network. diff --git a/ngo-fabric/README.md b/ngo-fabric/README.md index 48a02107..9efc9a15 100644 --- a/ngo-fabric/README.md +++ b/ngo-fabric/README.md @@ -418,3 +418,4 @@ The workshop instructions can be found in the README files in parts 1-4: * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. +* [Part 5:](../new-member/README.md) Add a new member to the network. diff --git a/ngo-rest-api/README.md b/ngo-rest-api/README.md index 7c1c21e5..f7e78f7f 100644 --- a/ngo-rest-api/README.md +++ b/ngo-rest-api/README.md @@ -225,3 +225,4 @@ The workshop instructions can be found in the README files in parts 1-4: * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. +* [Part 5:](../new-member/README.md) Add a new member to the network. diff --git a/ngo-ui/README.md b/ngo-ui/README.md index 6288a874..6899ad5f 100644 --- a/ngo-ui/README.md +++ b/ngo-ui/README.md @@ -152,3 +152,4 @@ The workshop instructions can be found in the README files in parts 1-4: * [Part 2:](../ngo-chaincode/README.md) Deploy the NGO chaincode. * [Part 3:](../ngo-rest-api/README.md) Run the REST API. * [Part 4:](../ngo-ui/README.md) Run the Application. +* [Part 5:](../new-member/README.md) Add a new member to the network. From 727dc716e4d048b44fb4bf3855e32da6251d671a Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Mon, 25 Mar 2019 12:07:24 +0800 Subject: [PATCH 14/15] add part 5 --- new-member/README.md | 54 +++++++++++--------- new-member/create-config-update.sh | 5 +- new-member/s3-handler.sh | 2 +- ngo-chaincode/README.md | 2 +- ngo-fabric/README.md | 2 +- ngo-rest-api/Troubleshooting.md | 17 +++++- ngo-rest-api/app.js | 2 +- ngo-ui/src/app/ui/signup/signup.component.ts | 6 +-- 8 files changed, 57 insertions(+), 33 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index 15412c02..f3a99fee 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -7,7 +7,7 @@ member can take part in endorsing transactions. The architecture of a multi-member Fabric network on Amazon Managed Blockchain is shown in the figure below. -![Multi-member Fabric network on Amazon Managed Blockchain](images/MultimemberNetwork.png "Multi-member Fabric network on Amazon Managed Blockchain") +![Multi-member Fabric network on Amazon Managed Blockchain](../images/MultimemberNetwork.png "Multi-member Fabric network on Amazon Managed Blockchain") Adding a new member to an existing Fabric network involves a number of steps. The new member will be located in a different AWS account, and the steps therefore involve cooperation between Fabric administrators in the existing account (let’s call this Account A, where the Fabric network was originally created), and the new account (let’s call this Account B, where the new member will be created). The steps look as follows: @@ -34,10 +34,10 @@ Adding a new member to an existing Fabric network involves a number of steps. Th There are two potential scenarios when adding new members to an existing Fabric network: ### The new member joins an existing channel -In this case, the member needs to be added to the channel configuration for an existing channel. To do this, follow all of the steps above. +In this case, the member needs to be added to the channel configuration for an existing channel. To do this, follow all of the steps in this README. ### A new channel is created with both new members and existing members -In this case a new channel configuration can be created which contains both new and existing members. This configuration can be used to create a new channel. To do this, follow these steps from those listed above: +In this case a new channel configuration can be created which contains both new and existing members. This configuration can be used to create a new channel. To do this, follow these steps in this README: * Steps 1-9 * Step 12b @@ -46,11 +46,9 @@ In this case a new channel configuration can be created which contains both new This scenario is simpler as it does not involve updating the existing channel configuration. ## Pre-requisites - Account A, the network creator -There are multiple parts to the workshop. Before starting on Part 5, a network creator should haved completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. +There are multiple parts to the workshop. Before starting on Part 5, a network creator should have completed [Part 1](../ngo-fabric/README.md). You need an existing Fabric network before starting Part 5. The network creator would have also created a peer node under a member belonging to Account A. -From Cloud9, SSH into the Fabric client node. The key (i.e. the .PEM file) should be in your home directory. -The DNS of the Fabric client node EC2 instance can be found in the output of the CloudFormation stack you -created in [Part 1](../ngo-fabric/README.md) +In the AWS account where you create the [Part 1](../ngo-fabric/README.md) Fabric network, use Cloud9 to SSH into the Fabric client node. The key (i.e. the .PEM file) should be in your home directory. The DNS of the Fabric client node EC2 instance can be found in the output of the CloudFormation stack you created in [Part 1](../ngo-fabric/README.md) ``` ssh ec2-user@ -i ~/-keypair.pem @@ -157,7 +155,7 @@ echo $VPCENDPOINTSERVICENAME ``` If the VPC endpoint is populated with a value, go ahead and run this script. This will create the -CloudFormation stack. You will see an error saying `Keypair not found`. This is expected as the script +CloudFormation stack. You will see an error saying `key pair does not exist`. This is expected as the script will check whether the keypair exists before creating it. I don't want to overwrite any existing keypairs you have, so just ignore this error and let the script continue: @@ -284,7 +282,7 @@ cd ~/non-profit-blockchain ## Step 7: Account A creates an MSP folder for the new Account B member On the Fabric client node in Account A. -Account A stores the certificates provided by Account B on its Fabric client node. +Account A stores the certificates provided by Account B on its Fabric client node. The script below will create a new MSP directory for the Account B member on the Account A Fabric client node. Update the region and member ID in the following script. The member ID is the ID of the new member in Account B, so this file should look identical to the one created in the previous step: @@ -296,11 +294,17 @@ vi new-member/s3-handler.sh Copy the Account B public keys from S3 to the MSP directory on the Fabric client node in Account A: -```bash +``` cd ~/non-profit-blockchain ./new-member/s3-handler.sh copyCertsFromS3 ``` +Check the new MSP directory. The directory name will be the lowercase member ID: + +``` +ls -l ~ +``` + ## Step 8: Account A creates a configtx.yaml which includes the new Account B member On the Fabric client node in Account A. @@ -314,7 +318,7 @@ vi ~/configtx.yaml You will make two changes to the file. A working example of the updated configtx.yaml file can be found below the template example below: -1. Add Org2 (or Org3, Org4, etc. - just copy &Org2 under Organizations from the template below), replacing `Member2ID` with the ID of your member as copied directly from the Amazon Managed Blockchain console. For the MSPDir, `Member2ID` must be replaced by a lowercase member ID. Instead of typing this, you can copy it from your Fabric client node. Enter: `ls ~` and find the directory with your lower case member ID. The end result of the MSPDir in configtx.yaml should look as follows: `MSPDir: /opt/home/m-trd4xpjborem7bdmbv6wlg3nkm-msp` +1. Add Org2 (or Org3, Org4, etc. - just copy &Org2 under Organizations from the template below), replacing `Member2ID` with the ID of the new member from Account B as copied directly from the Amazon Managed Blockchain console. For the MSPDir, `Member2ID` must be replaced by a lowercase member ID. Instead of typing this, you can copy it from the Fabric client node in Account A, where you created the MSP dir in Step 7. Enter: `ls ~` and find the directory with your lower case member ID. The end result of the MSPDir in configtx.yaml should look as follows: `MSPDir: /opt/home/m-trd4xpjborem7bdmbv6wlg3nkm-msp` 2. Add the TwoOrgChannel under Organizations, under Profiles, at the end of the file. Save the file. @@ -503,10 +507,17 @@ ls -lt ~/$CHANNEL-two-org.pb ``` ### Print the new member configuration -Generate a member definition for the new member. It reads the information for the member from configtx.yaml, created in the previous step: +Generate a member definition for the new member. It reads the information for the member from configtx.yaml, created in the previous step. Replace the value of NEWMEMBERID below: + +First, export the new member id, from Account B: + +``` +export NEWMEMBERID= +``` + +Then generate the new member config: ``` -export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM docker exec cli /bin/bash -c "configtxgen -printOrg $NEWMEMBERID --configPath /opt/home/ > /tmp/$NEWMEMBERID.json" ``` @@ -526,11 +537,7 @@ $ docker exec cli ls -lt /tmp/$NEWMEMBERID.json ``` ### Fetch the latest configuration block from the channel -The channel creation block for the channel exists in the location below. It was created just prior to the creation of the channel in [Part 1:](../ngo-fabric/README.md). - -``` -ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer -``` +The channel creation block for the channel was created in [Part 1:](../ngo-fabric/README.md), but just in case the configuration was updated at some point, we will pull the latest version of the config block directly from the channel. ``` docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ @@ -539,7 +546,7 @@ docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt -c $CHANNEL -o $ORDERER --cafile /opt/home/managedblockchain-tls-chain.pem --tls ``` -Check that the latest config block file now exists: +Check that the latest config block file exists. The latest config block will be called $CHANNEL.config.block, whereas the original config block (used to create the genesis block) will be called $CHANNEL.block: ``` ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer @@ -555,8 +562,10 @@ cp create-config-update.sh ~ A utility program called 'configtxlator' is used to create the new channel config - it translates the JSON channel configurations to/from binary protobuf structures. 'configtxlator' can run as a REST API server, so we start it in this mode since we need to make a few calls to it. +Replace MEMBERID in the statement below with the member ID of the new member in Account B: + ``` -docker exec -e "CHANNEL=mychannel" -e "MEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM" -e "BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer" cli /opt/home/create-config-update.sh +docker exec -e "CHANNEL=mychannel" -e "MEMBERID=" -e "BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer" cli /opt/home/create-config-update.sh ``` You should see: @@ -620,7 +629,6 @@ Since our network currently contains only one member, the network creator, this To allow admins belonging to different members to sign the channel configuration you will need to pass the channel configuration ‘diff’ file to each member in the network, one-by-one, and have them sign the channel configuration. Each member signature must be applied in turn so that we end up with a package that has the signatures of all endorsing members. Alternatively, you could send the channel config to all members simultaneously and wait to receive signed responses, but then you would have to extract the signatures from the individual responses and create a single package which contains the configuration update plus all the required signatures. ``` -export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM export BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ @@ -639,11 +647,9 @@ Once we have a signed channel configuration we can apply it to the channel. On the Fabric client node in Account A. In this step we update the channel with the new channel configuration. Since the new channel configuration now includes details -of the new organisation, this will allow the new organisation to join the channel. +of the new member, this will allow the new member to join the channel. ``` -export NEWMEMBERID=m-TRD4XPJBOREM7BDMBV6WLG3NKM -export BLOCKDIR=/opt/home/fabric-samples/chaincode/hyperledger/fabric/peer docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ cli bash -c "peer channel update -f ${BLOCKDIR}/${NEWMEMBERID}_config_update_as_envelope.pb -c $CHANNEL -o $ORDERER --cafile /opt/home/managedblockchain-tls-chain.pem --tls" diff --git a/new-member/create-config-update.sh b/new-member/create-config-update.sh index 4a4bcc3d..be720c82 100755 --- a/new-member/create-config-update.sh +++ b/new-member/create-config-update.sh @@ -25,7 +25,10 @@ function createConfigUpdate { sleep 5 pushd /tmp - + # Remove any previously generated config or protobuf files + rm /tmp/${MEMBERID}_config*.* + rm /tmp/${MEMBERID}_updated*.* + CTLURL=http://127.0.0.1:7059 # Convert the config block protobuf to JSON curl -X POST --data-binary @$BLOCKDIR/$CHANNEL.config.block $CTLURL/protolator/decode/common.Block > ${MEMBERID}_config_block.json diff --git a/new-member/s3-handler.sh b/new-member/s3-handler.sh index 8e43ad02..711fef58 100755 --- a/new-member/s3-handler.sh +++ b/new-member/s3-handler.sh @@ -16,7 +16,7 @@ set +e region=us-east-1 -memberID=m-TRD4XPJBOREM7BDMBV6WLG3NKM +memberID= # convert memberID to lowercase. S3 buckets must be lower case memberID=$(echo "$memberID" | tr '[:upper:]' '[:lower:]') diff --git a/ngo-chaincode/README.md b/ngo-chaincode/README.md index a96eeed9..08454eb9 100644 --- a/ngo-chaincode/README.md +++ b/ngo-chaincode/README.md @@ -91,7 +91,7 @@ Expected response: ## Step 3 - Instantiate the chaincode on the channel -Instantiation initlizes the chaincode on the channel, i.e. it binds the chaincode to a specific channel. +Instantiation initializes the chaincode on the channel, i.e. it binds the chaincode to a specific channel. Instantiation is treated as a Fabric transaction. In fact, when chaincode is instantiated, the Init function on the chaincode is called. Instantiation also sets the endorsement policy for this version of the chaincode on this channel. In the example below we are not explictly passing an endorsement policy, so the default diff --git a/ngo-fabric/README.md b/ngo-fabric/README.md index 9efc9a15..0bd39293 100644 --- a/ngo-fabric/README.md +++ b/ngo-fabric/README.md @@ -87,7 +87,7 @@ echo $VPCENDPOINTSERVICENAME ``` If the VPC endpoint is populated with a value, go ahead and run this script. This will create the -CloudFormation stack. You will see an error saying `Keypair not found`. This is expected as the script +CloudFormation stack. You will see an error saying `key pair does not exist`. This is expected as the script will check whether the keypair exists before creating it. I don't want to overwrite any existing keypairs you have, so just ignore this error and let the script continue: diff --git a/ngo-rest-api/Troubleshooting.md b/ngo-rest-api/Troubleshooting.md index c573b5ee..c70ff1a3 100644 --- a/ngo-rest-api/Troubleshooting.md +++ b/ngo-rest-api/Troubleshooting.md @@ -8,4 +8,19 @@ error is caused by using the wrong certificate - probably an old one from the ce [2018-11-16T10:25:40.240] [ERROR] Connection - ##### getRegisteredUser - Failed to get registered user: 5742cbbe-03b6-449d-ab65-3c885b6bfee1 with error: Error: Enrollment failed with errors [[{"code":19,"message":"CA 'ca.esxh3vewtnhsrldv5du3p52zpq' does not exist"}]] -We need to the name of the Fabric CA, as set in the CA, in FABRIC_CA_SERVER_CA_NAME \ No newline at end of file +We need to set the name of the Fabric CA, as set in the CA, in FABRIC_CA_SERVER_CA_NAME + + + +##### invokeChaincode - Invoke transaction request to Fabric {"targets":["peer1"],"chaincodeId":"ngo","fcn":"createSpend","args":["{\"spendId\":\"43a4d8be-c9f7-4d45-9f25-6074d312ee47\",\"spendDescription\":\"Peter Pipers Poulty Portions for Pets\",\"spendDate\":\"2018-09-20T12:41:59.582Z\",\"spendAmount\":22}"],"chainId":"mychannel","txId":{"_nonce":{"type":"Buffer","data":[78,51,96,123,70,26,73,101,108,20,68,49,246,213,77,198,106,37,113,217,60,230,97,118]},"_transaction_id":"c600ae42fc8ef3dffb50a2b27710d2b6488cf7bc2a90032299be78220ae0a113","_admin":false}} +[2019-03-15T05:41:41.344] [ERROR] Invoke - ##### invokeChaincode - received unsuccessful proposal response +[2019-03-15T05:41:41.344] [INFO] Invoke - ##### invokeChaincode - Failed to send Proposal and receive all good ProposalResponse. Status code: undefined, 2 UNKNOWN: access denied: channel [mychannel] creator org [org1MSP] +Error: 2 UNKNOWN: access denied: channel [mychannel] creator org [org1MSP] + + +The identities used by the REST API are being cached. Remove the cache directories: + +``` +rm -rf fabric-client-kv-org1/ +rm -rf /tmp/fabric-client-kv-org1/ +``` \ No newline at end of file diff --git a/ngo-rest-api/app.js b/ngo-rest-api/app.js index fc399916..02dbecbf 100644 --- a/ngo-rest-api/app.js +++ b/ngo-rest-api/app.js @@ -49,7 +49,7 @@ var channelName = hfc.getConfigSetting('channelName'); var chaincodeName = hfc.getConfigSetting('chaincodeName'); var peers = hfc.getConfigSetting('peers'); /////////////////////////////////////////////////////////////////////////////// -//////////////////////////////// SET CONFIGURATONS //////////////////////////// +//////////////////////////////// SET CONFIGURATIONS /////////////////////////// /////////////////////////////////////////////////////////////////////////////// app.options('*', cors()); app.use(cors()); diff --git a/ngo-ui/src/app/ui/signup/signup.component.ts b/ngo-ui/src/app/ui/signup/signup.component.ts index 968a79da..bf38cd84 100644 --- a/ngo-ui/src/app/ui/signup/signup.component.ts +++ b/ngo-ui/src/app/ui/signup/signup.component.ts @@ -72,17 +72,17 @@ export class SignupComponent implements OnInit { }, err => { this.loading = false; - this.error = 'Username or email already in used!'; + this.error = 'Username or email already in use!'; } ); } else { this.loading = false; - this.error = 'Username or email already in used!'; + this.error = 'Username or email already in use!'; } }, err => { this.loading = false; - this.error = 'Username or email already in used!'; + this.error = 'Username or email already in use!'; } ); } From f77dfb5c4816c4f2633091f8aebc2c0fc0518573 Mon Sep 17 00:00:00 2001 From: Michael Edge Date: Mon, 25 Mar 2019 13:38:28 +0800 Subject: [PATCH 15/15] add part 5 --- new-member/README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/new-member/README.md b/new-member/README.md index f3a99fee..127eb9e4 100644 --- a/new-member/README.md +++ b/new-member/README.md @@ -616,7 +616,7 @@ total 36 ## Step 10: Endorsing peers must sign the new channel configuration On the Fabric client node in Account A. -In this step, when we refer to 'diff', we mean the binary protobuf version of the 'diff' file, which we wrapped in an envelope in the final step in the script `create-config-update.sh`. This file is titled `m-TRD4XPJBOREM7BDMBV6WLG3NKM_config_update_as_envelope.pb` in the previous step, and can be found by executing this command: +In this step, when we refer to 'diff', we mean the binary protobuf version of the 'diff' file, which we wrapped in an envelope in the final step in the script `create-config-update.sh`. This file is titled `_config_update_as_envelope.pb` in the previous step, and can be found by executing this command: ``` ls -lt /home/ec2-user/fabric-samples/chaincode/hyperledger/fabric/peer @@ -669,8 +669,7 @@ On the Fabric client node in Account A. Before the peer node in Account B joins the channel, it must be able to connect to the Ordering Service managed by Amazon Managed Blockchain. The peer obtains the Ordering Service endpoint from the channel genesis block. The file mychannel.block ('mychannel' refers to the channel name and may differ if you have changed the channel name) would have been created when you first created the channel in [Part 1](../ngo-fabric/README.md). Make sure the mychannel.block file is available to the peer node in Account B. -Copy the channel genesis from from Account A to S3: - +On the Fabric client node in Account A, copy the channel genesis from from Account A to S3: ```bash cd ~/non-profit-blockchain @@ -747,7 +746,7 @@ You should see: ## Step 15: Account B queries the chaincode On the Fabric client node in Account B. -Query the chaincode on Fabric peer. +Query the chaincode on Fabric peer. This may take 30-60s as the peer node must create and build the Docker image used for hosting the chaincode. Execute the following script: @@ -799,8 +798,15 @@ We can resolve the invalid transaction in two ways: 1. Send the `peer chaincode invoke` transaction to peer nodes in Account A and Account B. Since Account A is able to endorse transactions, the statement below should work. It will obtain endorsements from peers in Account A and Account B. When the endorsement policy is checked during the validation step, it will succeed as the transaction has an endorsement from a member included in the policy. If you review [Part 1](../ngo-fabric/README.md) where we instantiated the chaincode, we did not provide an endorsement policy, so the default endorsement policy would have been applied. In this case it would be `“OR(‘Org1.member’)”`. +In the statement below, replace the values of `peerAddresses` with the addresses of the two peers nodes, one from Account A and the other from Account B. You can obtain the peer address endpoints from the Managed Blockchain console. + ``` -docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" cli peer chaincode invoke -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -c '{"Args":["invoke","a","b","10"]}' --cafile $CAFILE --tls --peerAddresses nd-JZMD3XB7R5ARXA5HVM4GPZ2CFI.m-TEW3EJGTPBBW7BMGXYOIXV5364.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30003 --peerAddresses nd-KSQ2ACC3PZBNBNBPYWKUEAEABY.m-TRD4XPJBOREM7BDMBV6WLG3NKM.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30006 --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem +docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode invoke -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -c '{"Args":["invoke","a","b","10"]}' \ + --cafile $CAFILE --tls --peerAddresses nd-JZMD3XB7R5ARXA5HVM4GPZ2CFI.m-TEW3EJGTPBBW7BMGXYOIXV5364.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30003 \ + --peerAddresses nd-KSQ2ACC3PZBNBNBPYWKUEAEABY.m-TRD4XPJBOREM7BDMBV6WLG3NKM.n-BAWJYVPCQBE5ZKAM323FNRG3ZU.managedblockchain.us-east-1.amazonaws.com:30006 \ + --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem --tlsRootCertFiles /opt/home/managedblockchain-tls-chain.pem ``` 2. Add the new member to the endorsement policy. To do this, the chaincode on the channel needs to be updated with a new endorsement policy that includes the new member. This will be done in the next step. @@ -829,13 +835,14 @@ You should see: Then upgrade the new chaincode on the channel. Upgrading chaincode is very similar to instantiating, and should take around 30 seconds to complete as Fabric creates a new Docker chaincode container, installs the chaincode, and calls the 'init' function. Keep this in mind when you design your own chaincode: the 'init' function is going to run each time the chaincode is upgraded, so don't make the mistake you see in the fabric-samples where the state is initialised in the ‘init’ function. This results in the current world state being reset to the values defined in the ‘init’ function each time the chaincode is upgraded. Best practice for Fabric is to have a separate function to initialise the state, and only call this once, during chaincode instantiation. -Before running this, change the members in your endorsement policy to match your own member IDs. You can find the member IDs for both Account A and Account B in the Amazon Managed Blockchain console, or you can look in Step 7 above where you added both member IDs to configtx.yaml, in the section `Organizations->Name`. +Before running this, change the members in your endorsement policy to match your own member IDs. You can find the member IDs for both Account A and Account B in the Managed Blockchain console, or you can look in Step 7 above where you added both member IDs to configtx.yaml, in the section `Organizations->Name`. ``` -docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ - -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ - cli peer chaincode upgrade -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -v $CHAINCODEVERSION \ - -c '{"Args":["init","a","100","b","200"]}' --cafile $CAFILE --tls -P "OR('m-TEW3EJGTPBBW7BMGXYOIXV5364.member','m-TRD4XPJBOREM7BDMBV6WLG3NKM.member')" + docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \ + -e "CORE_PEER_ADDRESS=$PEER" -e "CORE_PEER_LOCALMSPID=$MSP" -e "CORE_PEER_MSPCONFIGPATH=$MSP_PATH" \ + cli peer chaincode upgrade -o $ORDERER -C $CHANNEL -n $CHAINCODENAME -v $CHAINCODEVERSION \ + -c '{"Args":["init","a","100","b","200"]}' --cafile $CAFILE --tls \ + -P "OR('.member','.member')" ``` You should see: @@ -850,7 +857,7 @@ Now query and invoke transactions to confirm that the upgrade was successful. ## Step 18: Account B installs the latest version of the chaincode On the Fabric client node in Account B. -If you run the query statement again it will fail. This is because the peer node in Account B does not have the latest version of the chaincode: +If you run the query statement from Step 15 on Account B it will fail. This is because the peer node in Account B does not have the latest version of the chaincode: ``` $ docker exec -e "CORE_PEER_TLS_ENABLED=true" -e "CORE_PEER_TLS_ROOTCERT_FILE=/opt/home/managedblockchain-tls-chain.pem" \