├── integration ├── data │ ├── asset-transfer-basic-go.tgz │ ├── asset-transfer-basic-java.tgz │ ├── asset-transfer-basic-javascript.tgz │ └── asset-transfer-basic-typescript.tgz └── integration_suite_test.go ├── cmd ├── microfab │ └── main.go └── microfabd │ └── main.go ├── docker ├── couchdb-rpm.repo ├── local.ini └── docker-entrypoint.sh ├── tools └── tools.go ├── internal ├── pkg │ ├── peer │ │ ├── peer_suite_test.go │ │ ├── endorser.go │ │ ├── deliver.go │ │ ├── chaincode.go │ │ ├── connection.go │ │ ├── peer_test.go │ │ ├── channel.go │ │ ├── peer.go │ │ └── runtime.go │ ├── txid │ │ ├── txid_suite_test.go │ │ ├── txid_test.go │ │ └── txid.go │ ├── orderer │ │ ├── orderer_suite_test.go │ │ ├── broadcast.go │ │ ├── deliver.go │ │ ├── orderer_test.go │ │ ├── connection.go │ │ └── orderer.go │ ├── organization │ │ ├── organization_suite_test.go │ │ ├── organization_test.go │ │ └── organization.go │ ├── blocks │ │ ├── blocks_suite_test.go │ │ ├── blocks.go │ │ └── fakes │ │ │ └── deliverer.go │ ├── identity │ │ ├── certificate │ │ │ └── certificate.go │ │ ├── privatekey │ │ │ └── privatekey.go │ │ └── identity.go │ ├── couchdb │ │ ├── couchdb.go │ │ └── proxy.go │ ├── config │ │ └── config.go │ ├── util │ │ └── util.go │ ├── ca │ │ ├── connection.go │ │ ├── ca.go │ │ └── runtime.go │ ├── channel │ │ ├── chaincode.go │ │ └── channel.go │ ├── configtxlator │ │ └── update.go │ ├── protoutil │ │ └── protoutil.go │ └── proxy │ │ └── proxy.go └── app │ └── microfabd │ └── config.go ├── scripts ├── lint.sh └── test-container.sh ├── Makefile ├── .github ├── settings.yml └── workflows │ ├── pr.yml │ └── publish.yml ├── MAINTAINERS.md ├── example.json ├── examples ├── two-orgs.json ├── registerEnrollUser.sh └── justfile ├── .dockerignore ├── .gitignore ├── pkg ├── microfab │ ├── ping.go │ ├── root.go │ ├── stop.go │ ├── start.go │ ├── connect.go │ └── util.go └── client │ └── client.go ├── docs ├── ConnectingClients.md ├── ConfiguringMicrofab.md └── Tutorial.md ├── go.mod ├── README.md ├── Dockerfile └── Dockerfile2 /integration/data/asset-transfer-basic-go.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledgendary/microfab/main/integration/data/asset-transfer-basic-go.tgz -------------------------------------------------------------------------------- /integration/data/asset-transfer-basic-java.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledgendary/microfab/main/integration/data/asset-transfer-basic-java.tgz -------------------------------------------------------------------------------- /cmd/microfab/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hyperledger-labs/microfab/pkg/microfab" 4 | 5 | func main() { 6 | microfab.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /integration/data/asset-transfer-basic-javascript.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledgendary/microfab/main/integration/data/asset-transfer-basic-javascript.tgz -------------------------------------------------------------------------------- /integration/data/asset-transfer-basic-typescript.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledgendary/microfab/main/integration/data/asset-transfer-basic-typescript.tgz -------------------------------------------------------------------------------- /docker/couchdb-rpm.repo: -------------------------------------------------------------------------------- 1 | [couchdb] 2 | name=couchdb 3 | baseurl=https://apache.jfrog.io/artifactory/couchdb-rpm/el$releasever/$basearch/ 4 | gpgkey=https://couchdb.apache.org/repo/keys.asc https://couchdb.apache.org/repo/rpm-package-key.asc 5 | gpgcheck=1 6 | repo_gpgcheck=1 7 | enabled=1 -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | package tools 9 | 10 | import ( 11 | _ "github.com/go-task/slim-sprig" 12 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 13 | _ "golang.org/x/lint/golint" 14 | _ "sourcegraph.com/sqs/goreturns" 15 | ) 16 | -------------------------------------------------------------------------------- /docker/local.ini: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | [couchdb] 5 | database_dir=/opt/microfab/data/couchdb/data 6 | single_node=true 7 | view_index_dir=/opt/microfab/data/couchdb/data 8 | [log] 9 | writer = file 10 | file = /opt/microfab/data/couchdb/logs/couchdb.log 11 | level = info 12 | [admins] 13 | admin=adminpw 14 | -------------------------------------------------------------------------------- /internal/pkg/peer/peer_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestPeer(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Peer Suite") 17 | } 18 | -------------------------------------------------------------------------------- /internal/pkg/txid/txid_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package txid_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestTxid(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "TxID Suite") 17 | } 18 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | set -euxo pipefail 6 | GOFMT="$(gofmt -l -s .)" 7 | test -z "${GOFMT}" 8 | go run golang.org/x/lint/golint -set_exit_status ./... 9 | GORETURNS="$(go run sourcegraph.com/sqs/goreturns -l cmd internal)" 10 | test -z "${GORETURNS}" 11 | go vet ./... 12 | -------------------------------------------------------------------------------- /internal/pkg/orderer/orderer_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestOrderer(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Orderer Suite") 17 | } 18 | -------------------------------------------------------------------------------- /integration/integration_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package integration_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestIntegration(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Integration Suite") 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | .PHONY: all lint unit integration 6 | 7 | all: lint unit 8 | 9 | generate: 10 | go generate ./... 11 | 12 | lint: 13 | ./scripts/lint.sh 14 | 15 | unit: 16 | go run github.com/onsi/ginkgo/ginkgo -skipPackage integration ./... 17 | 18 | integration: 19 | go run github.com/onsi/ginkgo/ginkgo integration 20 | -------------------------------------------------------------------------------- /internal/pkg/organization/organization_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package organization_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestOrganization(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Organization Suite") 17 | } 18 | -------------------------------------------------------------------------------- /internal/pkg/blocks/blocks_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package blocks_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/deliverer.go --fake-name Deliverer . Deliverer 15 | 16 | func TestBlocks(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "Blocks Suite") 19 | } 20 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | repository: 6 | name: microfab 7 | description: Microfab is a containerized Hyperledger Fabric runtime for use in development environments 8 | default_branch: main 9 | has_downloads: false 10 | has_issues: true 11 | has_projects: false 12 | has_wiki: false 13 | archived: false 14 | private: false 15 | allow_squash_merge: true 16 | allow_merge_commit: false 17 | allow_rebase_merge: true 18 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Maintainers 2 | 3 | ### Active Maintainers 4 | | name | Github | Discord | 5 | |-------------------|--------------------------------------------------------|--------------| 6 | | Dave Enyeart | [@denyeart](https://github.com/denyeart) | denyeart#0989 | 7 | | James Taylor | [@jt-nti](https://github.com/jt-nti) | jtonline#5082 | 8 | | Matthew B White | [@mbwhite](https://github.com/mbwhite) | calanais#7776 | 9 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "endorsing_organizations": [ 3 | { 4 | "name": "org1" 5 | }, 6 | { 7 | "name": "org2" 8 | } 9 | ], 10 | "channels": [ 11 | { 12 | "name": "mychannel", 13 | "endorsing_organizations": [ 14 | "org1" 15 | ] 16 | }, 17 | { 18 | "name": "appchannel", 19 | "endorsing_organizations": [ 20 | "org1", 21 | "org2" 22 | ] 23 | } 24 | ], 25 | "capability_level": "V2_5" 26 | } -------------------------------------------------------------------------------- /examples/two-orgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "endorsing_organizations": [ 3 | { 4 | "name": "org1" 5 | }, 6 | { 7 | "name": "org2" 8 | } 9 | ], 10 | "channels": [ 11 | { 12 | "name": "mychannel", 13 | "endorsing_organizations": [ 14 | "org1" 15 | ] 16 | }, 17 | { 18 | "name": "appchannel", 19 | "endorsing_organizations": [ 20 | "org1", 21 | "org2" 22 | ] 23 | } 24 | ], 25 | "capability_level": "V2_5" 26 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Directories created by us at runtime 22 | /data 23 | 24 | # Other stuff not meant to go into the image 25 | .dockerignore 26 | .gitignore 27 | azure-pipelines.yml 28 | Dockerfile 29 | LICENSE 30 | README.md -------------------------------------------------------------------------------- /cmd/microfabd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/hyperledger-labs/microfab/internal/app/microfabd" 13 | ) 14 | 15 | var logger = log.New(os.Stdout, fmt.Sprintf("[%16s] ", "microfabd"), log.LstdFlags) 16 | 17 | func main() { 18 | microfabd, err := microfabd.New() 19 | if err != nil { 20 | logger.Fatalf("Failed to create application: %v", err) 21 | } 22 | err = microfabd.Start() 23 | if err != nil { 24 | logger.Fatalf("Failed to start application: %v", err) 25 | } 26 | microfabd.Wait() 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | # Binaries for programs and plugins 6 | /bin 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | # Directories created by us at runtime 23 | /data 24 | 25 | /builders/java/.classpath 26 | /builders/java/.project 27 | /builders/java/.settings 28 | /builders/java/target 29 | 30 | .vscode 31 | 32 | microfabd 33 | 34 | _mfcfg 35 | 36 | -------------------------------------------------------------------------------- /internal/pkg/peer/endorser.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/hyperledger/fabric-protos-go/peer" 12 | ) 13 | 14 | // ProcessProposal asks the peer to endorse the specified proposal. 15 | func (c *Connection) ProcessProposal(signedProposal *peer.SignedProposal) (*peer.ProposalResponse, error) { 16 | endorserClient := peer.NewEndorserClient(c.clientConn) 17 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 18 | defer cancel() 19 | response, err := endorserClient.ProcessProposal(ctx, signedProposal) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return response, nil 24 | } 25 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | set -euo pipefail 6 | 7 | # If this image is run with -u , as happens on Red Hat OpenShift, then 8 | # the user is not in the /etc/passwd file. This causes Ansible to fail, so we need 9 | # to add the user to /etc/passwd now before Ansible runs. 10 | if ! whoami &> /dev/null; then 11 | sed '/microfab/d' /etc/passwd > /tmp/passwd 12 | cat /tmp/passwd > /etc/passwd 13 | rm -f /tmp/passwd 14 | echo "microfab:x:$(id -u):0::/home/microfab:/bin/bash" >> /etc/passwd 15 | export HOME=/home/microfab 16 | fi 17 | 18 | 19 | if [ -n "${MICROFAB_CONFIG:-}" ]; then 20 | COUCHDB_ENABLED=$(echo "${MICROFAB_CONFIG}" | jq -r '. | if has("couchdb") then .couchdb else true end') 21 | if [ "${COUCHDB_ENABLED}" = "true" ]; then 22 | couchdb & 23 | fi 24 | else 25 | couchdb & 26 | fi 27 | exec microfabd 28 | -------------------------------------------------------------------------------- /pkg/microfab/ping.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | 7 | "github.com/hyperledger-labs/microfab/pkg/client" 8 | "github.com/pkg/errors" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var pingCmd = &cobra.Command{ 13 | Use: "ping", 14 | Short: "Pings the microfab image to see if it's running", 15 | GroupID: "mf", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | return ping() 18 | }, 19 | } 20 | 21 | func ping() error { 22 | 23 | testURL, err := url.Parse("http://console.127-0-0-1.nip.io:8080") 24 | if err != nil { 25 | return errors.Errorf("Unable to parse URL %s", testURL.String()) 26 | } 27 | 28 | mfc, err := client.New(testURL, false) 29 | if err != nil { 30 | return errors.Wrapf(err, "Unable to connect create client to connect to Microfab") 31 | 32 | } 33 | err = mfc.Ping() 34 | if err != nil { 35 | return errors.Wrapf(err, "Unable to connect to runing microfab") 36 | } 37 | log.Println("Microfab ping successful") 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/microfab/root.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "microfab", 12 | Short: "microfab...", 13 | Long: "Microfab Launch Control", 14 | Version: "c0", 15 | SilenceUsage: true, 16 | SilenceErrors: true, 17 | } 18 | 19 | var defaultCfg = `{"endorsing_organizations":[{"name":"org1"}],"channels":[{"name":"mychannel","endorsing_organizations":["org1"]},{"name":"appchannel","endorsing_organizations":["org1"]}],"capability_level":"V2_5"}` 20 | 21 | var cfg string 22 | var mspdir string 23 | var force bool 24 | var cfgFile string 25 | 26 | // Execute the microfab command 27 | func Execute() { 28 | viper.AutomaticEnv() 29 | viper.ReadInConfig() 30 | 31 | if err := rootCmd.Execute(); err != nil { 32 | log.Fatalf("%s\n", err) 33 | } 34 | } 35 | 36 | func init() { 37 | 38 | rootCmd.AddGroup(&cobra.Group{ID: "mf", Title: "microfab"}) 39 | rootCmd.AddCommand(startCmd) 40 | rootCmd.AddCommand(stopCmd) 41 | rootCmd.AddCommand(connectCmd) 42 | rootCmd.AddCommand(pingCmd) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /internal/pkg/identity/certificate/certificate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package certificate 6 | 7 | import ( 8 | "crypto/sha256" 9 | "crypto/x509" 10 | "encoding/pem" 11 | ) 12 | 13 | // Certificate represents a loaded X509 certificate. 14 | type Certificate struct { 15 | certificate *x509.Certificate 16 | bytes []byte 17 | } 18 | 19 | // FromBytes loads an X509 certificate from PEM data. 20 | func FromBytes(data []byte) (*Certificate, error) { 21 | block, _ := pem.Decode(data) 22 | certificate, err := x509.ParseCertificate(block.Bytes) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &Certificate{certificate, data}, nil 27 | } 28 | 29 | // Certificate returns the X509 certificate. 30 | func (c *Certificate) Certificate() *x509.Certificate { 31 | return c.certificate 32 | } 33 | 34 | // Bytes returns the bytes of the X509 certificate. 35 | func (c *Certificate) Bytes() []byte { 36 | return c.bytes 37 | } 38 | 39 | // Hash returns a SHA256 hash over the bytes of the X509 certificate. 40 | func (c *Certificate) Hash() []byte { 41 | sha := sha256.New() 42 | sha.Write(c.bytes) 43 | return sha.Sum(nil) 44 | } 45 | -------------------------------------------------------------------------------- /internal/pkg/identity/privatekey/privatekey.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package privatekey 6 | 7 | import ( 8 | "crypto/ecdsa" 9 | "crypto/x509" 10 | "encoding/pem" 11 | "fmt" 12 | ) 13 | 14 | // PrivateKey represents a loaded ECDSA private key. 15 | type PrivateKey struct { 16 | privateKey *ecdsa.PrivateKey 17 | bytes []byte 18 | } 19 | 20 | // FromBytes loads an ECDSA private key from PEM data. 21 | func FromBytes(data []byte) (*PrivateKey, error) { 22 | block, _ := pem.Decode(data) 23 | temp, err := x509.ParsePKCS8PrivateKey(block.Bytes) 24 | if err != nil { 25 | return nil, err 26 | } 27 | privateKey, ok := temp.(*ecdsa.PrivateKey) 28 | if !ok { 29 | return nil, fmt.Errorf("The specified private key is not an ECDSA private key") 30 | } 31 | return &PrivateKey{privateKey, data}, nil 32 | } 33 | 34 | // PrivateKey returns the ECDSA private key. 35 | func (p *PrivateKey) PrivateKey() *ecdsa.PrivateKey { 36 | return p.privateKey 37 | } 38 | 39 | // PublicKey returns the ECDSA public key. 40 | func (p *PrivateKey) PublicKey() *ecdsa.PublicKey { 41 | return &p.privateKey.PublicKey 42 | } 43 | 44 | // Bytes returns the bytes of the ECDSA private key. 45 | func (p *PrivateKey) Bytes() []byte { 46 | return p.bytes 47 | } 48 | -------------------------------------------------------------------------------- /pkg/microfab/stop.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/docker/docker/api/types/container" 10 | "github.com/docker/docker/client" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var stopCmd = &cobra.Command{ 15 | Use: "stop", 16 | Short: "Stops the microfab image running", 17 | GroupID: "mf", 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | return Stop("microfab") 20 | }, 21 | } 22 | 23 | // Stop stops the container 24 | func Stop(containername string) error { 25 | ctx := context.Background() 26 | 27 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer cli.Close() 32 | 33 | log.Printf("Attempting to stop the container") 34 | 35 | if err := cli.ContainerStop(ctx, containername, container.StopOptions{}); err != nil { 36 | return errors.Wrapf(err, "Unable to stop container %s: %s", containername, err) 37 | } 38 | 39 | statusCh, errCh := cli.ContainerWait(ctx, containername, container.WaitConditionRemoved) 40 | select { 41 | case err := <-errCh: 42 | if err != nil { 43 | panic(err) 44 | } 45 | case <-statusCh: 46 | } 47 | 48 | log.Printf("Container stopped and removed") 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/pkg/txid/txid_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package txid_test 6 | 7 | import ( 8 | "crypto/sha256" 9 | "encoding/hex" 10 | 11 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 14 | "github.com/hyperledger/fabric-protos-go/msp" 15 | . "github.com/onsi/ginkgo" 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | var _ = Describe("the txid package", func() { 20 | 21 | var testIdentity *identity.Identity 22 | 23 | BeforeEach(func() { 24 | var err error 25 | testIdentity, err = identity.New("Org1Admin") 26 | Expect(err).NotTo(HaveOccurred()) 27 | }) 28 | 29 | Context("txid.New()", func() { 30 | 31 | When("called", func() { 32 | It("creates a new transaction ID", func() { 33 | transactionID := txid.New("Org1MSP", testIdentity) 34 | Expect(transactionID.MSPID()).To(Equal("Org1MSP")) 35 | Expect(transactionID.Identity()).To(Equal(testIdentity)) 36 | nonce := transactionID.Nonce() 37 | Expect(nonce).To(HaveLen(24)) 38 | serializedIdentity := &msp.SerializedIdentity{ 39 | Mspid: "Org1MSP", 40 | IdBytes: testIdentity.Certificate().Bytes(), 41 | } 42 | sha := sha256.New() 43 | sha.Write(nonce) 44 | sha.Write(util.MarshalOrPanic(serializedIdentity)) 45 | hash := sha.Sum(nil) 46 | expectedString := hex.EncodeToString(hash) 47 | Expect(transactionID.String()).To(Equal(expectedString)) 48 | }) 49 | }) 50 | 51 | }) 52 | 53 | }) 54 | -------------------------------------------------------------------------------- /internal/pkg/orderer/broadcast.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "github.com/hyperledger/fabric-protos-go/common" 14 | "github.com/hyperledger/fabric-protos-go/orderer" 15 | ) 16 | 17 | // Broadcast sends an envelope with one or more transactions in to the orderer. 18 | func (c *Connection) Broadcast(envelope *common.Envelope) error { 19 | abClient := orderer.NewAtomicBroadcastClient(c.clientConn) 20 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 21 | defer cancel() 22 | broadcastClient, err := abClient.Broadcast(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | eof := make(chan bool) 27 | responses := make(chan *orderer.BroadcastResponse) 28 | errors := make(chan error) 29 | go func() { 30 | for { 31 | response, err := broadcastClient.Recv() 32 | if err == io.EOF { 33 | eof <- true 34 | return 35 | } else if err != nil { 36 | errors <- err 37 | } else { 38 | responses <- response 39 | } 40 | } 41 | }() 42 | err = broadcastClient.Send(envelope) 43 | if err != nil { 44 | return err 45 | } 46 | err = broadcastClient.CloseSend() 47 | if err != nil { 48 | return err 49 | } 50 | done := false 51 | for !done { 52 | select { 53 | case err = <-errors: 54 | return err 55 | case response := <-responses: 56 | if response.GetStatus() != common.Status_SUCCESS { 57 | return fmt.Errorf("Bad status returned by ordering service %v", response.GetStatus()) 58 | } 59 | case <-eof: 60 | done = true 61 | } 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/pkg/couchdb/couchdb.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package couchdb 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | nurl "net/url" 11 | "time" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // CouchDB represents a CouchDB instance. 17 | type CouchDB struct { 18 | internalURL *nurl.URL 19 | externalURL *nurl.URL 20 | } 21 | 22 | // New creates a new CouchDB instance. 23 | func New(internalURL, externalURL string) (*CouchDB, error) { 24 | parsedInternalURL, err := nurl.Parse(internalURL) 25 | if err != nil { 26 | return nil, err 27 | } 28 | parsedExternalURL, err := nurl.Parse(externalURL) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &CouchDB{internalURL: parsedInternalURL, externalURL: parsedExternalURL}, nil 33 | } 34 | 35 | // WaitFor waits for the CouchDB instance to start. 36 | func (c *CouchDB) WaitFor(timeout time.Duration) error { 37 | timeoutCh := time.After(timeout) 38 | tick := time.Tick(250 * time.Millisecond) 39 | for { 40 | select { 41 | case <-timeoutCh: 42 | return errors.New("timeout whilst waiting for CouchDB to start") 43 | case <-tick: 44 | if c.hasStarted() { 45 | return nil 46 | } 47 | } 48 | } 49 | } 50 | 51 | // URL returns the URL of the CouchDB instance. 52 | func (c *CouchDB) URL(internal bool) *url.URL { 53 | if internal { 54 | return c.internalURL 55 | } 56 | return c.externalURL 57 | } 58 | 59 | func (c *CouchDB) hasStarted() bool { 60 | upURL := c.internalURL.ResolveReference(&url.URL{Path: "/_up"}).String() 61 | resp, err := http.Get(upURL) 62 | if err != nil { 63 | return false 64 | } 65 | if resp.StatusCode != 200 { 66 | return false 67 | } 68 | return true 69 | } 70 | -------------------------------------------------------------------------------- /internal/pkg/txid/txid.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package txid 6 | 7 | import ( 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/hex" 11 | 12 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 14 | "github.com/hyperledger/fabric-protos-go/msp" 15 | ) 16 | 17 | // TransactionID represents a generated transaction ID. 18 | type TransactionID struct { 19 | mspID string 20 | identity *identity.Identity 21 | txID string 22 | nonce []byte 23 | } 24 | 25 | // New creates a new transaction ID for the specified MSP ID and identity. 26 | func New(mspID string, identity *identity.Identity) *TransactionID { 27 | nonce := make([]byte, 24) 28 | _, err := rand.Read(nonce) 29 | if err != nil { 30 | panic(err) 31 | } 32 | serializedIdentity := &msp.SerializedIdentity{ 33 | Mspid: mspID, 34 | IdBytes: identity.Certificate().Bytes(), 35 | } 36 | sha := sha256.New() 37 | sha.Write(nonce) 38 | sha.Write(util.MarshalOrPanic(serializedIdentity)) 39 | hash := sha.Sum(nil) 40 | txID := hex.EncodeToString(hash) 41 | return &TransactionID{mspID, identity, txID, nonce} 42 | } 43 | 44 | // MSPID returns the MSP ID used to create this transaction ID. 45 | func (t *TransactionID) MSPID() string { 46 | return t.mspID 47 | } 48 | 49 | // Identity returns the identity used to create this transaction ID. 50 | func (t *TransactionID) Identity() *identity.Identity { 51 | return t.identity 52 | } 53 | 54 | // String returns the transaction ID as a string. 55 | func (t *TransactionID) String() string { 56 | return t.txID 57 | } 58 | 59 | // Nonce returns the nonce used to create this transaction ID. 60 | func (t *TransactionID) Nonce() []byte { 61 | return t.nonce 62 | } 63 | -------------------------------------------------------------------------------- /examples/registerEnrollUser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xeu -o pipefail 4 | DIR=-"$(cd "$(dirname "${BASH_SOURCE[0]}" )"/.. && pwd )" 5 | 6 | # Register and Enroll a New user 7 | 8 | # expect path is correct setup 9 | 10 | `export FABRIC_CA_CLIENT_HOME=${dir}/test-network/organizations/peerOrganizations/org1.example.com/ && ` + 11 | `fabric-ca-client register --caname ca-org1 --id.name owner --id.secret ownerpw --id.type client --tls.certfiles "${dir}/test-network/organizations/fabric-ca/org1/tls-cert.pem" && ` + 12 | `fabric-ca-client enroll -u https://owner:ownerpw@localhost:7054 --caname ca-org1 -M "${dir}/test-network/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp" --tls.certfiles "${dir}/test-network/organizations/fabric-ca/org1/tls-cert.pem" && ` + 13 | `cp "${dir}/test-network/organizations/peerOrganizations/org1.example.com/msp/config.yaml" "${dir}/test-network/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp/config.yaml" && ` + 14 | `export FABRIC_CA_CLIENT_HOME=${dir}/test-network/organizations/peerOrganizations/org2.example.com/ && ` + 15 | `fabric-ca-client register --caname ca-org2 --id.name buyer --id.secret buyerpw --id.type client --tls.certfiles "${dir}/test-network/organizations/fabric-ca/org2/tls-cert.pem" && ` + 16 | `fabric-ca-client enroll -u https://buyer:buyerpw@localhost:8054 --caname ca-org2 -M "${dir}/test-network/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp" --tls.certfiles "${dir}/test-network/organizations/fabric-ca/org2/tls-cert.pem" && ` + 17 | `cp "${dir}/test-network/organizations/peerOrganizations/org2.example.com/msp/config.yaml" "${dir}/test-network/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp/config.yaml"`; 18 | execSync(cmd); 19 | -------------------------------------------------------------------------------- /internal/pkg/orderer/deliver.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "github.com/hyperledger-labs/microfab/internal/pkg/blocks" 14 | "github.com/hyperledger/fabric-protos-go/common" 15 | "github.com/hyperledger/fabric-protos-go/orderer" 16 | ) 17 | 18 | // Deliver requests one or more blocks from the orderer. 19 | func (c *Connection) Deliver(envelope *common.Envelope, callback blocks.DeliverCallback) error { 20 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 21 | defer cancel() 22 | deliverClient, err := orderer.NewAtomicBroadcastClient(c.clientConn).Deliver(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | eof := make(chan bool) 27 | responses := make(chan *orderer.DeliverResponse) 28 | errors := make(chan error) 29 | go func() { 30 | for { 31 | response, err := deliverClient.Recv() 32 | if err == io.EOF { 33 | eof <- true 34 | return 35 | } else if err != nil { 36 | errors <- err 37 | } else { 38 | responses <- response 39 | } 40 | } 41 | }() 42 | err = deliverClient.Send(envelope) 43 | if err != nil { 44 | return err 45 | } 46 | err = deliverClient.CloseSend() 47 | if err != nil { 48 | return err 49 | } 50 | done := false 51 | for !done { 52 | select { 53 | case err = <-errors: 54 | return err 55 | case response := <-responses: 56 | switch response.GetType().(type) { 57 | case *orderer.DeliverResponse_Status: 58 | if response.GetStatus() != common.Status_SUCCESS { 59 | return fmt.Errorf("Bad status returned by orderer: %v", response.GetStatus()) 60 | } 61 | case *orderer.DeliverResponse_Block: 62 | block := response.GetBlock() 63 | err = callback(block) 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | case <-eof: 69 | done = true 70 | } 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/pkg/peer/deliver.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "github.com/hyperledger-labs/microfab/internal/pkg/blocks" 14 | "github.com/hyperledger/fabric-protos-go/common" 15 | "github.com/hyperledger/fabric-protos-go/peer" 16 | ) 17 | 18 | // Deliver requests one or more blocks from the peer. 19 | func (c *Connection) Deliver(envelope *common.Envelope, callback blocks.DeliverCallback) error { 20 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 21 | defer cancel() 22 | deliverClient, err := peer.NewDeliverClient(c.clientConn).Deliver(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | eof := make(chan bool) 27 | responses := make(chan *peer.DeliverResponse) 28 | errors := make(chan error) 29 | go func() { 30 | for { 31 | response, err := deliverClient.Recv() 32 | if err == io.EOF { 33 | eof <- true 34 | return 35 | } else if err != nil { 36 | errors <- err 37 | } else { 38 | responses <- response 39 | } 40 | } 41 | }() 42 | err = deliverClient.Send(envelope) 43 | if err != nil { 44 | return err 45 | } 46 | err = deliverClient.CloseSend() 47 | if err != nil { 48 | return err 49 | } 50 | done := false 51 | for !done { 52 | select { 53 | case err = <-errors: 54 | return err 55 | case response := <-responses: 56 | switch response.GetType().(type) { 57 | case *peer.DeliverResponse_Status: 58 | if response.GetStatus() != common.Status_SUCCESS { 59 | return fmt.Errorf("Bad status returned by peer: %v", response.GetStatus()) 60 | } 61 | case *peer.DeliverResponse_Block: 62 | block := response.GetBlock() 63 | err = callback(block) 64 | if err != nil { 65 | return err 66 | } 67 | case *peer.DeliverResponse_FilteredBlock: 68 | return fmt.Errorf("Filtered block returned by peer when block expected") 69 | } 70 | case <-eof: 71 | done = true 72 | } 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. 2017 All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package config 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/gogo/protobuf/proto" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/blocks" 14 | "github.com/hyperledger-labs/microfab/internal/pkg/configtxlator" 15 | "github.com/hyperledger/fabric-protos-go/common" 16 | ) 17 | 18 | // GetConfigEnvelope gets the latest config envelope from the specified channel. 19 | func GetConfigEnvelope(deliverer blocks.Deliverer, channel string) (*common.ConfigEnvelope, error) { 20 | configBlock, err := blocks.GetConfigBlock(deliverer, channel) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if len(configBlock.Data.Data) != 1 { 25 | return nil, fmt.Errorf("Config block must only contain one transaction") 26 | } 27 | envelope := &common.Envelope{} 28 | err = proto.Unmarshal(configBlock.Data.Data[0], envelope) 29 | if err != nil { 30 | return nil, err 31 | } 32 | payload := &common.Payload{} 33 | err = proto.Unmarshal(envelope.Payload, payload) 34 | if err != nil { 35 | return nil, err 36 | } 37 | configEnvelope := &common.ConfigEnvelope{} 38 | err = proto.Unmarshal(payload.Data, configEnvelope) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return configEnvelope, nil 43 | } 44 | 45 | // GetConfig gets the latest config from the specified channel. 46 | func GetConfig(deliverer blocks.Deliverer, channel string) (*common.Config, error) { 47 | configEnvelope, err := GetConfigEnvelope(deliverer, channel) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return configEnvelope.GetConfig(), nil 52 | } 53 | 54 | // GenerateConfigUpdate generates a config update calculated by comparing the specified configs. 55 | func GenerateConfigUpdate(originalConfig *common.Config, newConfig *common.Config) (*common.ConfigUpdate, error) { 56 | configUpdate, err := configtxlator.Compute(originalConfig, newConfig) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return configUpdate, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/pkg/organization/organization_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package organization_test 6 | 7 | import ( 8 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("the organization package", func() { 14 | 15 | Context("organization.New()", func() { 16 | 17 | When("called with a name with no special characters", func() { 18 | It("creates a new organization", func() { 19 | o, err := organization.New("Org1", nil, nil) 20 | Expect(err).NotTo(HaveOccurred()) 21 | Expect(o.Name()).To(Equal("Org1")) 22 | Expect(o.MSPID()).To(Equal("Org1MSP")) 23 | ca := o.CA() 24 | Expect(ca).NotTo(BeNil()) 25 | Expect(ca.Certificate().Certificate().Subject.CommonName).To(Equal("Org1 CA")) 26 | Expect(ca.Certificate().Certificate().IsCA).To(BeTrue()) 27 | admin := o.Admin() 28 | Expect(admin).NotTo(BeNil()) 29 | Expect(admin.Certificate().Certificate().Subject.CommonName).To(Equal("Org1 Admin")) 30 | Expect(admin.Certificate().Certificate().IsCA).To(BeFalse()) 31 | err = admin.Certificate().Certificate().CheckSignatureFrom(ca.Certificate().Certificate()) 32 | Expect(err).NotTo(HaveOccurred()) 33 | }) 34 | }) 35 | 36 | When("called with a name with special characters", func() { 37 | It("creates a new organization", func() { 38 | o, err := organization.New("Org @ 1", nil, nil) 39 | Expect(err).NotTo(HaveOccurred()) 40 | Expect(o.Name()).To(Equal("Org @ 1")) 41 | Expect(o.MSPID()).To(Equal("Org1MSP")) 42 | ca := o.CA() 43 | Expect(ca).NotTo(BeNil()) 44 | Expect(ca.Certificate().Certificate().Subject.CommonName).To(Equal("Org @ 1 CA")) 45 | Expect(ca.Certificate().Certificate().IsCA).To(BeTrue()) 46 | admin := o.Admin() 47 | Expect(admin).NotTo(BeNil()) 48 | Expect(admin.Certificate().Certificate().Subject.CommonName).To(Equal("Org @ 1 Admin")) 49 | Expect(admin.Certificate().Certificate().IsCA).To(BeFalse()) 50 | err = admin.Certificate().Certificate().CheckSignatureFrom(ca.Certificate().Certificate()) 51 | Expect(err).NotTo(HaveOccurred()) 52 | }) 53 | }) 54 | 55 | }) 56 | 57 | }) 58 | -------------------------------------------------------------------------------- /internal/pkg/organization/organization.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package organization 6 | 7 | import ( 8 | "fmt" 9 | "regexp" 10 | 11 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 12 | ) 13 | 14 | // Organization represents a loaded organization definition. 15 | type Organization struct { 16 | name string 17 | ca *identity.Identity 18 | admin *identity.Identity 19 | caAdmin *identity.Identity 20 | mspID string 21 | tlsCA *identity.Identity 22 | } 23 | 24 | // New creates a new organization. 25 | func New(name string, ca *identity.Identity, tlsCA *identity.Identity) (*Organization, error) { 26 | if ca == nil { 27 | caName := fmt.Sprintf("%s CA", name) 28 | var err error 29 | ca, err = identity.New(caName, identity.WithIsCA(true)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | } 34 | adminName := fmt.Sprintf("%s Admin", name) 35 | admin, err := identity.New(adminName, identity.WithOrganizationalUnit("admin"), identity.UsingSigner(ca)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | safeRegex := regexp.MustCompile("[^a-zA-Z0-9]+") 40 | safeName := safeRegex.ReplaceAllString(name, "") 41 | mspID := fmt.Sprintf("%sMSP", safeName) 42 | return &Organization{name, ca, admin, nil, mspID, tlsCA}, nil 43 | } 44 | 45 | // Name returns the name of the organization. 46 | func (o *Organization) Name() string { 47 | return o.name 48 | } 49 | 50 | // MSPID returns the MSP ID for the organization. 51 | func (o *Organization) MSPID() string { 52 | return o.mspID 53 | } 54 | 55 | // CA returns the CA for the organization. 56 | func (o *Organization) CA() *identity.Identity { 57 | return o.ca 58 | } 59 | 60 | // Admin returns the admin identity for the organization. 61 | func (o *Organization) Admin() *identity.Identity { 62 | return o.admin 63 | } 64 | 65 | // CAAdmin returns the CA admin identity for the organization. 66 | func (o *Organization) CAAdmin() *identity.Identity { 67 | return o.caAdmin 68 | } 69 | 70 | // SetCAAdmin adds an identity to the organization. 71 | func (o *Organization) SetCAAdmin(id *identity.Identity) { 72 | o.caAdmin = id 73 | } 74 | 75 | // GetIdentities returns all identities for the organization. 76 | func (o *Organization) GetIdentities() []*identity.Identity { 77 | result := []*identity.Identity{o.admin} 78 | if o.caAdmin != nil { 79 | result = append(result, o.caAdmin) 80 | } 81 | return result 82 | } 83 | -------------------------------------------------------------------------------- /docs/ConnectingClients.md: -------------------------------------------------------------------------------- 1 | # Connecting Clients 2 | 3 | ## Fabric Gateway Clients 4 | 5 | As an example look at the the [`trader-typescript` example](https://github.com/hyperledger/fabric-samples/tree/main/full-stack-asset-transfer-guide/applications/trader-typescript). Specifically the [`config.ts` file](https://github.com/hyperledger/fabric-samples/blob/main/full-stack-asset-transfer-guide/applications/trader-typescript/src/config.ts). 6 | 7 | Though is is typescript the information that is needed is the same irrespective of language. Any typical application will need to know the following information. In the example this is using envronment variables 8 | 9 | | | | 10 | |----------------|------------------------------------------------------------------| 11 | | ENDPOINT | Endpoint address of the gateway service | 12 | | MSP_ID | User's organization Member Services Provider ID | 13 | | CERTIFICATE | User's certificate file | 14 | | PRIVATE_KEY | User's private key file | 15 | | CHANNEL_NAME | Channel to which the chaincode is deployed | 16 | | CHAINCODE_NAME | Channel to which the chaincode is deployed | 17 | | TLS_CERT | TLS CA root certificate (only if using TLS and private CA) | 18 | | HOST_ALIAS | TLS hostname override (only if TLS cert does not match endpoint) | 19 | 20 | In the [tutorial](./Tutorial.md) Microfab was started as follows 21 | 22 | ``` 23 | docker run -d --rm -p 8080:8080 --name microfab ghcr.io/hyperledger-labs/microfab:latest 24 | curl -s http://console.127-0-0-1.nip.io:8080/ak/api/v1/components | npx @hyperledger-labs/weft microfab -w _wallets -p _gateways -m _msp -f 25 | ``` 26 | 27 | To setup the environment for the Peer Command (nonTLS): 28 | ``` 29 | export CORE_PEER_LOCALMSPID=Org1MSP 30 | export CORE_PEER_MSPCONFIGPATH=$(pwd)/_msp/Org1/org1admin/msp 31 | export CORE_PEER_ADDRESS=org1peer-api.127-0-0-1.nip.io:8080 32 | ``` 33 | 34 | To setup the environment for the example gateway application (nonTLS) 35 | 36 | ``` 37 | export ENDPOINT=org1peer-api.127-0-0-1.nip.io:8080 38 | export MSP_ID=Org1MSP 39 | export CERTIFICATE=$(pwd)/_msp/org1/org1admin/msp/admincerts/cert.pem 40 | export PRIVATE_KEY=$(pwd)/_msp/org1/org1admin/msp/keystore/cert_sk 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /internal/pkg/peer/chaincode.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/hyperledger-labs/microfab/internal/pkg/protoutil" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 13 | "github.com/hyperledger/fabric-protos-go/common" 14 | "github.com/hyperledger/fabric-protos-go/peer" 15 | "github.com/hyperledger/fabric-protos-go/peer/lifecycle" 16 | ) 17 | 18 | // InstallChaincode installs the specified chaincode package onto the peer. 19 | func (c *Connection) InstallChaincode(pkg []byte) (string, error) { 20 | txID := txid.New(c.mspID, c.identity) 21 | channelHeader := protoutil.BuildChannelHeader(common.HeaderType_ENDORSER_TRANSACTION, "", txID) 22 | cche := &peer.ChaincodeHeaderExtension{ 23 | ChaincodeId: &peer.ChaincodeID{ 24 | Name: "_lifecycle", 25 | }, 26 | } 27 | channelHeader.Extension = util.MarshalOrPanic(cche) 28 | signatureHeader := protoutil.BuildSignatureHeader(txID) 29 | header := &common.Header{ 30 | ChannelHeader: util.MarshalOrPanic(channelHeader), 31 | SignatureHeader: util.MarshalOrPanic(signatureHeader), 32 | } 33 | cciSpec := &peer.ChaincodeInvocationSpec{ 34 | ChaincodeSpec: &peer.ChaincodeSpec{ 35 | Type: peer.ChaincodeSpec_GOLANG, 36 | ChaincodeId: &peer.ChaincodeID{ 37 | Name: "_lifecycle", 38 | }, 39 | Input: &peer.ChaincodeInput{ 40 | Args: [][]byte{ 41 | []byte("InstallChaincode"), 42 | util.MarshalOrPanic(&lifecycle.InstallChaincodeArgs{ 43 | ChaincodeInstallPackage: pkg, 44 | }), 45 | }, 46 | }, 47 | }, 48 | } 49 | ccpp := &peer.ChaincodeProposalPayload{ 50 | Input: util.MarshalOrPanic(cciSpec), 51 | } 52 | proposal := &peer.Proposal{ 53 | Header: util.MarshalOrPanic(header), 54 | Payload: util.MarshalOrPanic(ccpp), 55 | } 56 | proposalBytes := util.MarshalOrPanic(proposal) 57 | signature := c.identity.Sign(proposalBytes) 58 | signedProposal := &peer.SignedProposal{ 59 | ProposalBytes: proposalBytes, 60 | Signature: signature, 61 | } 62 | response, err := c.ProcessProposal(signedProposal) 63 | if err != nil { 64 | return "", err 65 | } else if response.Response.Status != int32(common.Status_SUCCESS) { 66 | return "", fmt.Errorf("Bad proposal response: status %d, mesage %s", response.Response.Status, response.Response.Message) 67 | } 68 | result := &lifecycle.InstallChaincodeResult{} 69 | util.UnmarshalOrPanic(response.Response.Payload, result) 70 | return result.PackageId, nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/pkg/orderer/orderer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer_test 6 | 7 | import ( 8 | "io/ioutil" 9 | 10 | "github.com/hyperledger-labs/microfab/internal/pkg/orderer" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("the orderer package", func() { 17 | 18 | var testOrganization *organization.Organization 19 | var testDirectory string 20 | 21 | BeforeEach(func() { 22 | var err error 23 | testOrganization, err = organization.New("Org1", nil, nil) 24 | Expect(err).NotTo(HaveOccurred()) 25 | testDirectory, err = ioutil.TempDir("", "ut-peer") 26 | Expect(err).NotTo(HaveOccurred()) 27 | }) 28 | 29 | Context("orderer.New()", func() { 30 | 31 | When("called", func() { 32 | It("creates a new peer", func() { 33 | p, err := orderer.New(testOrganization, testDirectory, 8080, 7051, "grpc://orderer-api.127-0-0-1.nip.io:8080", 8443, "http://orderer-operations.127-0-0-1.nip.io:8080") 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(p.Organization()).To(Equal(testOrganization)) 36 | Expect(p.MSPID()).To(Equal(testOrganization.MSPID())) 37 | Expect(p.APIHostname(false)).To(Equal("orderer-api.127-0-0-1.nip.io")) 38 | Expect(p.APIHostname(true)).To(Equal("localhost")) 39 | Expect(p.APIHost(false)).To(Equal("orderer-api.127-0-0-1.nip.io:8080")) 40 | Expect(p.APIHost(true)).To(Equal("localhost:7051")) 41 | Expect(p.APIPort(false)).To(BeEquivalentTo(8080)) 42 | Expect(p.APIPort(true)).To(BeEquivalentTo(7051)) 43 | Expect(p.APIURL(false).String()).To(BeEquivalentTo("grpc://orderer-api.127-0-0-1.nip.io:8080")) 44 | Expect(p.APIURL(true).String()).To(BeEquivalentTo("grpc://localhost:7051")) 45 | Expect(p.OperationsHost(false)).To(Equal("orderer-operations.127-0-0-1.nip.io:8080")) 46 | Expect(p.OperationsHost(true)).To(Equal("localhost:8443")) 47 | Expect(p.OperationsPort(false)).To(BeEquivalentTo(8080)) 48 | Expect(p.OperationsPort(true)).To(BeEquivalentTo(8443)) 49 | Expect(p.OperationsURL(false).String()).To(BeEquivalentTo("http://orderer-operations.127-0-0-1.nip.io:8080")) 50 | Expect(p.OperationsURL(true).String()).To(BeEquivalentTo("http://localhost:8443")) 51 | }) 52 | }) 53 | 54 | When("called with an invalid API URL", func() { 55 | It("returns an error", func() { 56 | _, err := orderer.New(testOrganization, testDirectory, 8080, 7051, "!@£$%^&*()_+", 8443, "http://orderer-operations.127-0-0-1.nip.io:8080") 57 | Expect(err).To(HaveOccurred()) 58 | }) 59 | }) 60 | 61 | When("called with an invalid operations URL", func() { 62 | It("returns an error", func() { 63 | _, err := orderer.New(testOrganization, testDirectory, 8080, 7051, "grpc://orderer-api.127-0-0-1.nip.io:8080", 8443, "!@£$%^&*()_+") 64 | Expect(err).To(HaveOccurred()) 65 | }) 66 | }) 67 | 68 | }) 69 | 70 | }) 71 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | --- 6 | name: Pull Request 7 | on: 8 | pull_request: 9 | workflow_dispatch: {} 10 | 11 | jobs: 12 | 13 | # Lint, Compile and Core Function Tests 14 | build: 15 | name: Build and test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Use Go 1.18 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: 1.18 24 | - name: Run Go formatters and linters 25 | run: make lint 26 | - uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '11' 30 | - name: Use Node.js 12 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: 12 34 | - name: Install Fabric dependencies 35 | run: | 36 | sudo mkdir -p /opt/fabric 37 | pushd /opt/fabric 38 | curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary --fabric-version 2.5.0-alpha3 --ca-version 1.5.6-beta3 39 | popd 40 | cd builders/java 41 | sudo mvn -q dependency:copy-dependencies -DoutputDirectory=/opt/fabric-chaincode-java/lib 42 | npm install -g fabric-shim@2.5.1 43 | - name: Run Go formatters and linters 44 | run: make lint 45 | - name: Run Go unit tests 46 | run: make unit 47 | - name: Run Go integration tests 48 | run: | 49 | export PATH="/opt/fabric/bin:${PATH}" 50 | export FABRIC_CFG_PATH=/opt/fabric/config 51 | 52 | make integration 53 | - name: Run ShellCheck 54 | run: shellcheck $(ls builders/*/bin/* -1 | grep -v ccaas) 55 | 56 | fvt: 57 | name: Functional Tests 58 | needs: [build] 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Set up Docker Buildx 64 | uses: docker/setup-buildx-action@v2 65 | - name: Setup Fabric 66 | run: | 67 | curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary 68 | 69 | # set the path and cfg env var for the rest of the step 70 | echo "FABRIC_CFG_PATH=$GITHUB_WORKSPACE/config" >> $GITHUB_ENV 71 | echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH 72 | 73 | - name: Build Docker image 74 | id: dockerbuild 75 | uses: docker/build-push-action@v3 76 | with: 77 | context: . 78 | file: Dockerfile2 79 | platforms: linux/amd64 80 | push: false 81 | load: true 82 | tags: dev-microfab 83 | 84 | 85 | - name: Run against image 86 | run: | 87 | docker images 88 | ./scripts/test-container.sh 89 | env: 90 | MICROFAB_IMAGE: dev-microfab 91 | 92 | -------------------------------------------------------------------------------- /internal/pkg/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package util 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | 12 | "github.com/golang/protobuf/proto" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 14 | ) 15 | 16 | // GetHomeDirectory returns the Microfab home directory. 17 | func GetHomeDirectory() (string, error) { 18 | home, ok := os.LookupEnv("MICROFAB_HOME") 19 | if !ok { 20 | var err error 21 | home, err = os.Getwd() 22 | if err != nil { 23 | return "", err 24 | } 25 | } 26 | return home, nil 27 | } 28 | 29 | // MarshalOrPanic marshals the specified Protocol Buffer message into a byte array, and panics on failure. 30 | func MarshalOrPanic(pb proto.Message) []byte { 31 | res, err := proto.Marshal(pb) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return res 36 | } 37 | 38 | // UnmarshalOrPanic unmarshals the specified byte array into a Protocol Buffer message, and panics on failure. 39 | func UnmarshalOrPanic(b []byte, m proto.Message) { 40 | err := proto.Unmarshal(b, m) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | const config = `NodeOUs: 47 | Enable: true 48 | ClientOUIdentifier: 49 | Certificate: cacerts/ca.pem 50 | OrganizationalUnitIdentifier: client 51 | AdminOUIdentifier: 52 | Certificate: cacerts/ca.pem 53 | OrganizationalUnitIdentifier: admin 54 | PeerOUIdentifier: 55 | Certificate: cacerts/ca.pem 56 | OrganizationalUnitIdentifier: peer 57 | OrdererOUIdentifier: 58 | Certificate: cacerts/ca.pem 59 | OrganizationalUnitIdentifier: orderer 60 | ` 61 | 62 | // CreateMSPDirectory creates an MSP directory on disk suitable for the peer or orderer to use. 63 | func CreateMSPDirectory(directory string, identity *identity.Identity) error { 64 | directories := []string{ 65 | directory, 66 | path.Join(directory, "admincerts"), 67 | path.Join(directory, "cacerts"), 68 | path.Join(directory, "keystore"), 69 | path.Join(directory, "signcerts"), 70 | } 71 | for _, directory := range directories { 72 | err := os.MkdirAll(directory, 0755) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | err := ioutil.WriteFile(path.Join(directory, "config.yaml"), []byte(config), 0644) 78 | if err != nil { 79 | return err 80 | } 81 | privateKey := identity.PrivateKey().Bytes() 82 | err = ioutil.WriteFile(path.Join(directory, "keystore", "key.pem"), privateKey, 0644) 83 | if err != nil { 84 | return err 85 | } 86 | certificate := identity.Certificate().Bytes() 87 | err = ioutil.WriteFile(path.Join(directory, "signcerts", "cert.pem"), certificate, 0644) 88 | if err != nil { 89 | return err 90 | } 91 | if hasCA := identity.CA() != nil; hasCA { 92 | ca := identity.CA().Bytes() 93 | err = ioutil.WriteFile(path.Join(directory, "cacerts", "ca.pem"), ca, 0644) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hyperledger-labs/microfab 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/docker/docker v23.0.0+incompatible 7 | github.com/docker/go-connections v0.4.0 8 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 9 | github.com/gogo/protobuf v1.3.2 10 | github.com/golang/protobuf v1.5.2 11 | github.com/gorilla/mux v1.7.4 12 | github.com/hyperledger/fabric v2.1.0+incompatible 13 | github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e 14 | github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1 15 | github.com/onsi/ginkgo v1.16.4 16 | github.com/onsi/gomega v1.17.0 17 | github.com/pkg/errors v0.9.1 18 | github.com/spf13/cobra v1.6.1 19 | github.com/spf13/viper v1.15.0 20 | golang.org/x/exp v0.0.0-20230206171751-46f607a40771 21 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 22 | golang.org/x/net v0.4.0 23 | golang.org/x/sync v0.1.0 24 | google.golang.org/grpc v1.52.0 25 | gopkg.in/yaml.v2 v2.4.0 26 | sourcegraph.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 27 | ) 28 | 29 | require ( 30 | github.com/Knetic/govaluate v3.0.0+incompatible // indirect 31 | github.com/Microsoft/go-winio v0.6.0 // indirect 32 | github.com/Shopify/sarama v1.26.4 // indirect 33 | github.com/docker/distribution v2.8.2+incompatible // indirect 34 | github.com/docker/go-units v0.5.0 // indirect 35 | github.com/fsnotify/fsnotify v1.6.0 // indirect 36 | github.com/hashicorp/go-version v1.2.1 // indirect 37 | github.com/hashicorp/hcl v1.0.0 // indirect 38 | github.com/hyperledger/fabric-amcl v0.0.0-20200424173818-327c9e2cf77a // indirect 39 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 40 | github.com/magiconair/properties v1.8.7 // indirect 41 | github.com/miekg/pkcs11 v1.0.3 // indirect 42 | github.com/mitchellh/mapstructure v1.5.0 // indirect 43 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect 44 | github.com/morikuni/aec v1.0.0 // indirect 45 | github.com/nxadm/tail v1.4.8 // indirect 46 | github.com/opencontainers/go-digest v1.0.0 // indirect 47 | github.com/opencontainers/image-spec v1.0.2 // indirect 48 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 49 | github.com/spf13/afero v1.9.3 // indirect 50 | github.com/spf13/cast v1.5.0 // indirect 51 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 52 | github.com/spf13/pflag v1.0.5 // indirect 53 | github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect 54 | github.com/subosito/gotenv v1.4.2 // indirect 55 | github.com/sykesm/zap-logfmt v0.0.3 // indirect 56 | golang.org/x/mod v0.6.0 // indirect 57 | golang.org/x/sys v0.3.0 // indirect 58 | golang.org/x/text v0.5.0 // indirect 59 | golang.org/x/tools v0.2.0 // indirect 60 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect 61 | google.golang.org/protobuf v1.28.1 // indirect 62 | gopkg.in/ini.v1 v1.67.0 // indirect 63 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | gotest.tools/v3 v3.4.0 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /internal/pkg/couchdb/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package couchdb 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httputil" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | // Proxy represents a proxy to CouchDB. 19 | type Proxy struct { 20 | transport http.RoundTripper 21 | prefix string 22 | httpServer *http.Server 23 | } 24 | 25 | // NewProxy creates a new proxy to CouchDB. 26 | func (c *CouchDB) NewProxy(prefix string, port int) (*Proxy, error) { 27 | result := &Proxy{transport: http.DefaultTransport, prefix: prefix} 28 | director := func(req *http.Request) { 29 | req.URL.Scheme = c.internalURL.Scheme 30 | req.URL.Host = c.internalURL.Host 31 | if req.URL.Path == "/_all_dbs" { 32 | // Do nothing. 33 | } else if req.URL.Path == "/" || strings.HasPrefix(req.URL.Path, "/_") { 34 | // Do nothing. 35 | } else { 36 | // Rewrite to include the prefix. 37 | req.URL.Path = "/" + prefix + "_" + req.URL.Path[1:] 38 | if len(req.URL.RawPath) != 0 { 39 | req.URL.RawPath = "/" + prefix + "_" + req.URL.RawPath[1:] 40 | } 41 | } 42 | } 43 | reverseProxy := &httputil.ReverseProxy{ 44 | Director: director, 45 | Transport: result, 46 | FlushInterval: -1, 47 | } 48 | httpServer := &http.Server{ 49 | Addr: fmt.Sprintf(":%d", port), 50 | Handler: reverseProxy, 51 | } 52 | result.httpServer = httpServer 53 | return result, nil 54 | } 55 | 56 | // Start starts the proxy to CouchDB. 57 | func (p *Proxy) Start() error { 58 | return p.httpServer.ListenAndServe() 59 | } 60 | 61 | // Stop stops the proxy to CouchDB. 62 | func (p *Proxy) Stop() error { 63 | return p.httpServer.Close() 64 | } 65 | 66 | // RoundTrip handles a request to CouchDB. 67 | func (p *Proxy) RoundTrip(req *http.Request) (resp *http.Response, err error) { 68 | if req.URL.Path == "/_all_dbs" { 69 | return p.handleAllDbs(req) 70 | } 71 | return p.transport.RoundTrip(req) 72 | } 73 | 74 | func (p *Proxy) handleAllDbs(req *http.Request) (*http.Response, error) { 75 | resp, err := p.transport.RoundTrip(req) 76 | if err != nil { 77 | return nil, err 78 | } else if resp.StatusCode != 200 { 79 | return resp, nil 80 | } 81 | allDBs := []string{} 82 | err = json.NewDecoder(resp.Body).Decode(&allDBs) 83 | if err != nil { 84 | return nil, err 85 | } 86 | filteredDBs := []string{} 87 | for _, db := range allDBs { 88 | if strings.HasPrefix(db, "_") { 89 | filteredDBs = append(filteredDBs, db) 90 | } else if strings.HasPrefix(db, p.prefix+"_") { 91 | filteredDBs = append(filteredDBs, db[len(p.prefix)+1:]) 92 | } 93 | } 94 | data, err := json.Marshal(filteredDBs) 95 | if err != nil { 96 | return nil, err 97 | } 98 | resp.Body = ioutil.NopCloser(bytes.NewReader(data)) 99 | resp.ContentLength = int64(len(data)) 100 | resp.Header.Set("Content-Length", strconv.Itoa(len(data))) 101 | return resp, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/app/microfabd/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package microfabd 6 | 7 | import ( 8 | "encoding/json" 9 | "os" 10 | "path" 11 | "time" 12 | ) 13 | 14 | // Organization represents an organization in the configuration. 15 | type Organization struct { 16 | Name string `json:"name"` 17 | } 18 | 19 | // Channel represents a channel in the configuration. 20 | type Channel struct { 21 | Name string `json:"name"` 22 | EndorsingOrganizations []string `json:"endorsing_organizations"` 23 | CapabilityLevel string `json:"capability_level"` 24 | } 25 | 26 | // TLS represents the TLS configuration. 27 | type TLS struct { 28 | Enabled bool `json:"enabled"` 29 | Certificate *string `json:"certificate"` 30 | PrivateKey *string `json:"private_key"` 31 | CA *string `json:"ca"` 32 | } 33 | 34 | // Config represents the configuration. 35 | type Config struct { 36 | Domain string `json:"domain"` 37 | Port int `json:"port"` 38 | Directory string `json:"directory"` 39 | OrderingOrganization Organization `json:"ordering_organization"` 40 | EndorsingOrganizations []Organization `json:"endorsing_organizations"` 41 | Channels []Channel `json:"channels"` 42 | CapabilityLevel string `json:"capability_level"` 43 | CouchDB bool `json:"couchdb"` 44 | CertificateAuthorities bool `json:"certificate_authorities"` 45 | TimeoutString string `json:"timeout"` 46 | TLS TLS `json:"tls"` 47 | Timeout time.Duration `json:"-"` 48 | } 49 | 50 | // DefaultConfig returns the default configuration. 51 | func DefaultConfig() (*Config, error) { 52 | home, ok := os.LookupEnv("MICROFAB_HOME") 53 | if !ok { 54 | var err error 55 | home, err = os.Getwd() 56 | if err != nil { 57 | return nil, err 58 | } 59 | } 60 | config := &Config{ 61 | Domain: "127-0-0-1.nip.io", 62 | Port: 8080, 63 | Directory: path.Join(home, "data"), 64 | OrderingOrganization: Organization{ 65 | Name: "Orderer", 66 | }, 67 | EndorsingOrganizations: []Organization{ 68 | { 69 | Name: "Org1", 70 | }, 71 | }, 72 | Channels: []Channel{ 73 | { 74 | Name: "channel1", 75 | EndorsingOrganizations: []string{ 76 | "Org1", 77 | }, 78 | }, 79 | }, 80 | CapabilityLevel: "V2_5", 81 | CouchDB: true, 82 | CertificateAuthorities: true, 83 | TimeoutString: "30s", 84 | TLS: TLS{ 85 | Enabled: false, 86 | }, 87 | } 88 | if env, ok := os.LookupEnv("MICROFAB_CONFIG"); ok { 89 | err := json.Unmarshal([]byte(env), config) 90 | if err != nil { 91 | return nil, err 92 | } 93 | } 94 | if config.Port >= startPort && config.Port < endPort { 95 | logger.Fatalf("Cannot specify port %d, must be outside port range %d-%d", config.Port, 2000, 3000) 96 | } 97 | timeout, err := time.ParseDuration(config.TimeoutString) 98 | if err != nil { 99 | return nil, err 100 | } 101 | config.Timeout = timeout 102 | return config, nil 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microfab 2 | 3 | 'microfab' provides a single container image that allows you to quickly start Hyperledger Fabric when you are developing solutions. You can use it to rapidly iterate over changes to chaincode, and client applications. Configured with your selection of channels and orgs you want, it can be started and stopped in seconds. 4 | 5 | To learn how to use Microfab as part of the development workflow, follow the smart contract part of the [Hyperledger Fabric Sample's Full Stack Tutorial](https://github.com/hyperledger/fabric-samples/blob/main/full-stack-asset-transfer-guide/docs/SmartContractDev/00-Introduction.md) 6 | 7 | Check the [reference](./docs/DevelopingContracts.md) in this repo for details in other langauges. 8 | 9 | [![asciicast](https://asciinema.org/a/519913.svg)](https://asciinema.org/a/519913) 10 | 11 | 12 | ## Tutorial 13 | 14 | Check the [Quick Start Tutorial](./docs/Tutorial.md) - nothing to deployed smart contract in under 5minutes; 15 | 16 | ``` 17 | curl -sSL https://github.com/hyperledger-labs/microfab/releases/download/v0.0.18/microfab-linux-amd64 -o microfab 18 | microfab start --log 19 | ``` 20 | 21 | ## Why microfab? 22 | 23 | There are other 'form factors' of Fabric some are aimed at production/k8s deployments others more development focussed. 24 | 25 | - test-network with Fabric Samples - a docker-compose approach to starting fabric great for running the samples and as the 'reference standard' 26 | - minifab - also a good way of standing up a overall fabric network 27 | - test-network-nano - based around the separate binaries, useful when developing Fabric itself. 28 | 29 | Depending on your circumstances, familiarity and requirements different tools may be better. After running with Microfab in the Full Stack AssetTransfer workshop - Microfab is particularly good for the earlier contract and SDK development phases. 30 | 31 | ## What Fabric version does Microfab use? 32 | 33 | The idea is to have branches per release of Fabric. 34 | 35 | - `fabric-2.5` is Microfab using the 2.5 LTS for example. (and this is the default branch) 36 | - `fabric-2.4` uses thes (non-LTS) Fabric 2.4 37 | - `beta-3.0` will start to use Fabric 3.0 when it starts to become available 38 | 39 | ## Reference 40 | 41 | - [Configuring Microfab](./docs/ConfiguringMicrofab.md) 42 | - [Getting Started Tutorial](./docs/Tutorial.md) 43 | - [Connecting Clients](./docs/ConnectingClients.md) 44 | 45 | ### What Microfab can't do 46 | 47 | - Run in production, please just don't do it. It's development and test only 48 | - It doesn't yet support RAFT 49 | 50 | ### Unable to connect errors 51 | 52 | If you experience connection rejected type errors please check if your DNS is correctly able to resolve the `nip.io` addresses 53 | 54 | ``` 55 | ping server.127-0-0-1.nip.io 56 | PING server.127-0-0-1.nip.io (127.0.0.1) 56(84) bytes of data. 57 | 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.488 ms 58 | 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.035 ms 59 | ``` 60 | 61 | Some DNS servcies reject the DNS rewriting that this service uses and causes problems as you might image with microfab's proxy. 62 | A feature we'd to add is a detailed list of all the ports that internal services are using, so you expose them directly. Help appreciated! 63 | 64 | ## License 65 | 66 | Apache-2.0 67 | -------------------------------------------------------------------------------- /internal/pkg/peer/connection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "crypto/tls" 9 | "fmt" 10 | "net/url" 11 | 12 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 13 | "github.com/hyperledger-labs/microfab/pkg/client" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/credentials" 16 | ) 17 | 18 | // Connection represents a connection to a peer. 19 | type Connection struct { 20 | peer *Peer 21 | clientConn *grpc.ClientConn 22 | mspID string 23 | identity *identity.Identity 24 | } 25 | 26 | // Connect opens a connection to the peer. 27 | func Connect(peer *Peer, mspID string, identity *identity.Identity) (*Connection, error) { 28 | var clientConn *grpc.ClientConn 29 | var err error 30 | if peer.tls != nil { 31 | logger.Println("Peer TLS Enabled") 32 | creds := credentials.NewTLS(&tls.Config{ 33 | InsecureSkipVerify: true, 34 | }) 35 | clientConn, err = grpc.Dial(fmt.Sprintf("localhost:%d", peer.apiPort), grpc.WithTransportCredentials(creds)) 36 | } else { 37 | logger.Println("Peer _not_ TLS Enabled") 38 | clientConn, err = grpc.Dial(fmt.Sprintf("localhost:%d", peer.apiPort), grpc.WithInsecure()) 39 | } 40 | if err != nil { 41 | return nil, err 42 | } 43 | return &Connection{ 44 | peer: peer, 45 | clientConn: clientConn, 46 | mspID: mspID, 47 | identity: identity, 48 | }, nil 49 | } 50 | 51 | // ConnectClient opens a connection to the peer using a client peer object. 52 | func ConnectClient(peer *client.Peer, mspID string, identity *identity.Identity, tlsEnabled bool) (*Connection, error) { 53 | parsedURL, err := url.Parse(peer.APIURL) 54 | if err != nil { 55 | return nil, err 56 | } 57 | logger.Printf("ConnectionClient Peer has parsedURL %s\n", parsedURL) 58 | 59 | var clientConn *grpc.ClientConn 60 | 61 | if tlsEnabled { 62 | logger.Printf("Using TLS") 63 | creds := credentials.NewTLS(&tls.Config{ 64 | InsecureSkipVerify: true, 65 | }) 66 | 67 | clientConn, err = grpc.Dial(parsedURL.Host, grpc.WithTransportCredentials(creds), grpc.WithAuthority(peer.APIOptions.DefaultAuthority)) 68 | } else { 69 | logger.Printf("Not Using TLS") 70 | clientConn, err = grpc.Dial(parsedURL.Host, grpc.WithInsecure(), grpc.WithAuthority(peer.APIOptions.DefaultAuthority)) 71 | } 72 | 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return &Connection{ 78 | peer: nil, 79 | clientConn: clientConn, 80 | mspID: mspID, 81 | identity: identity, 82 | }, nil 83 | } 84 | 85 | // IsConnected returns true if the connection is open to the peer. 86 | func (c *Connection) IsConnected() bool { 87 | return c.clientConn != nil 88 | } 89 | 90 | // Close closes the connection to the peer. 91 | func (c *Connection) Close() error { 92 | c.identity = nil 93 | if c.clientConn != nil { 94 | err := c.clientConn.Close() 95 | c.clientConn = nil 96 | return err 97 | } 98 | return nil 99 | } 100 | 101 | // MSPID gets the MSP ID used for the connection. 102 | func (c *Connection) MSPID() string { 103 | return c.mspID 104 | } 105 | 106 | // Identity gets the identity used for the connection. 107 | func (c *Connection) Identity() *identity.Identity { 108 | return c.identity 109 | } 110 | -------------------------------------------------------------------------------- /internal/pkg/orderer/connection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer 6 | 7 | import ( 8 | "crypto/tls" 9 | "fmt" 10 | "log" 11 | "net/url" 12 | "os" 13 | 14 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 15 | "github.com/hyperledger-labs/microfab/pkg/client" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/credentials" 18 | ) 19 | 20 | var logger = log.New(os.Stdout, fmt.Sprintf("[%16s] ", "console"), log.LstdFlags) 21 | 22 | // Connection represents a connection to a orderer. 23 | type Connection struct { 24 | orderer *Orderer 25 | clientConn *grpc.ClientConn 26 | mspID string 27 | identity *identity.Identity 28 | } 29 | 30 | // Connect opens a connection to the orderer. 31 | func Connect(orderer *Orderer, mspID string, identity *identity.Identity) (*Connection, error) { 32 | var clientConn *grpc.ClientConn 33 | var err error 34 | if orderer.tls != nil { 35 | creds := credentials.NewTLS(&tls.Config{ 36 | InsecureSkipVerify: true, 37 | }) 38 | clientConn, err = grpc.Dial(fmt.Sprintf("localhost:%d", orderer.apiPort), grpc.WithTransportCredentials(creds)) 39 | } else { 40 | clientConn, err = grpc.Dial(fmt.Sprintf("localhost:%d", orderer.apiPort), grpc.WithInsecure()) 41 | } 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &Connection{ 46 | orderer: orderer, 47 | clientConn: clientConn, 48 | mspID: mspID, 49 | identity: identity, 50 | }, nil 51 | } 52 | 53 | // ConnectClient opens a connection to the orderer using a client orderer object. 54 | func ConnectClient(orderer *client.OrderingService, mspID string, identity *identity.Identity, tlsEnabled bool) (*Connection, error) { 55 | parsedURL, err := url.Parse(orderer.APIURL) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | var clientConn *grpc.ClientConn 61 | 62 | if tlsEnabled { 63 | logger.Printf("Using TLS") 64 | creds := credentials.NewTLS(&tls.Config{ 65 | InsecureSkipVerify: true, 66 | }) 67 | 68 | clientConn, err = grpc.Dial(parsedURL.Host, grpc.WithTransportCredentials(creds), grpc.WithAuthority(orderer.APIOptions.DefaultAuthority)) 69 | } else { 70 | logger.Printf("Not Using TLS") 71 | clientConn, err = grpc.Dial(parsedURL.Host, grpc.WithInsecure(), grpc.WithAuthority(orderer.APIOptions.DefaultAuthority)) 72 | } 73 | 74 | // clientConn, err := grpc.Dial(parsedURL.Host, grpc.WithInsecure(), grpc.WithAuthority(orderer.APIOptions.DefaultAuthority)) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return &Connection{ 79 | orderer: nil, 80 | clientConn: clientConn, 81 | mspID: mspID, 82 | identity: identity, 83 | }, nil 84 | } 85 | 86 | // IsConnected returns true if the connection is open to the orderer. 87 | func (c *Connection) IsConnected() bool { 88 | return c.clientConn != nil 89 | } 90 | 91 | // Close closes the connection to the orderer. 92 | func (c *Connection) Close() error { 93 | c.identity = nil 94 | if c.clientConn != nil { 95 | err := c.clientConn.Close() 96 | c.clientConn = nil 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | // MSPID gets the MSP ID used for the connection. 103 | func (c *Connection) MSPID() string { 104 | return c.mspID 105 | } 106 | 107 | // Identity gets the identity used for the connection. 108 | func (c *Connection) Identity() *identity.Identity { 109 | return c.identity 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | --- 6 | name: Publish Image 7 | on: 8 | create: 9 | tags: 10 | - "v*" 11 | workflow_dispatch: {} 12 | 13 | jobs: 14 | 15 | # Build the daemon binary and if a release publish if a 'tag' build 16 | # amd64/arm64 17 | binary_build: 18 | name: Binary Daemon Build 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | goarch: [amd64, arm64] 23 | env: 24 | GOARCH: ${{ matrix.goarch }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | - name: Use Go 1.18 29 | uses: actions/setup-go@v2 30 | with: 31 | go-version: 1.18 32 | - name: Build Binary 33 | run: go build -v -o bin/microfabd cmd/microfabd/main.go 34 | - name: Package Binary 35 | run: | 36 | export GOOS=$(go env GOOS) 37 | tar -C bin -czvf microfab-${GOOS}-${GOARCH}.tgz microfabd 38 | - name: Publish Binary to GitHub Release 39 | uses: softprops/action-gh-release@v1 40 | if: startsWith(github.ref, 'refs/tags/') 41 | with: 42 | files: microfab-*.tgz 43 | 44 | # Build the cli binary and if a release publish if a 'tag' build 45 | # amd64/arm64 46 | binary_cli_build: 47 | name: Binary CLI Build 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | goarch: [amd64, arm64] 52 | env: 53 | GOARCH: ${{ matrix.goarch }} 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v2 57 | - name: Use Go 1.18 58 | uses: actions/setup-go@v2 59 | with: 60 | go-version: 1.18 61 | - name: Build Binary 62 | run: go build -v -o bin/microfab-${GOARCH} cmd/microfab/main.go 63 | - name: Publish Binary to GitHub Release 64 | uses: softprops/action-gh-release@v1 65 | if: startsWith(github.ref, 'refs/tags/') 66 | with: 67 | files: bin/microfab-* 68 | 69 | # Build the container images and push to the ghcr.io repo 70 | # amd64/arm64 71 | container_build: 72 | runs-on: ubuntu-latest 73 | outputs: 74 | image_digest: ${{ steps.push.outputs.digest }} 75 | 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v3 79 | - name: Docker meta 80 | id: meta 81 | uses: docker/metadata-action@v4 82 | with: 83 | images: | 84 | ghcr.io/hyperledger-labs/microfab 85 | tags: | 86 | type=semver,pattern={{version}} 87 | type=semver,pattern={{major}}.{{minor}} 88 | type=semver,pattern={{major}} 89 | type=sha,format=long 90 | - name: Set up QEMU 91 | uses: docker/setup-qemu-action@v2 92 | - name: Set up Docker Buildx 93 | uses: docker/setup-buildx-action@v2 94 | - name: Login to GitHub Container Registry 95 | uses: docker/login-action@v2 96 | with: 97 | registry: ghcr.io 98 | username: ${{ github.repository_owner }} 99 | password: ${{ secrets.GITHUB_TOKEN }} 100 | - name: Build and push 101 | id: push 102 | uses: docker/build-push-action@v3 103 | with: 104 | context: . 105 | file: Dockerfile2 106 | platforms: linux/amd64,linux/arm64 107 | push: ${{ github.event_name != 'pull_request' }} 108 | tags: ${{ steps.meta.outputs.tags }} 109 | labels: ${{ steps.meta.outputs.labels }} 110 | -------------------------------------------------------------------------------- /internal/pkg/ca/connection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package ca 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ecdsa" 10 | "crypto/elliptic" 11 | "crypto/rand" 12 | "crypto/tls" 13 | "crypto/x509" 14 | "crypto/x509/pkix" 15 | "encoding/base64" 16 | "encoding/json" 17 | "encoding/pem" 18 | "fmt" 19 | "net/http" 20 | 21 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 22 | "github.com/hyperledger-labs/microfab/pkg/client" 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | // Connection represents a connection to a certificate authority. 27 | type Connection struct { 28 | ca *CA 29 | } 30 | 31 | // Connect opens a connection to the certificate authority. 32 | func Connect(ca *CA) (*Connection, error) { 33 | return &Connection{ca: ca}, nil 34 | } 35 | 36 | // Close closes the connection to the certificate authority. 37 | func (c *Connection) Close() error { 38 | return nil 39 | } 40 | 41 | // Enroll enrolls an identity using the certificate authority. 42 | func (c *Connection) Enroll(name, id, secret string) (*identity.Identity, error) { 43 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 44 | if err != nil { 45 | return nil, err 46 | } 47 | template := &x509.CertificateRequest{ 48 | Subject: pkix.Name{ 49 | CommonName: id, 50 | }, 51 | } 52 | csr, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey) 53 | if err != nil { 54 | return nil, err 55 | } 56 | csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}) 57 | request := map[string]interface{}{ 58 | "certificate_request": string(csrPEM), 59 | } 60 | data, err := json.Marshal(request) 61 | if err != nil { 62 | return nil, err 63 | } 64 | reader := bytes.NewReader(data) 65 | req, err := http.NewRequest("POST", fmt.Sprintf("%s/enroll", c.ca.APIURL(true)), reader) 66 | if err != nil { 67 | return nil, err 68 | } 69 | req.Header.Add("Content-Type", "application/json") 70 | req.SetBasicAuth(id, secret) 71 | cli := &http.Client{ 72 | Transport: &http.Transport{ 73 | TLSClientConfig: &tls.Config{ 74 | InsecureSkipVerify: true, 75 | }, 76 | }, 77 | } 78 | resp, err := cli.Do(req) 79 | if err != nil { 80 | return nil, err 81 | } 82 | if resp.StatusCode != 201 { 83 | return nil, errors.Errorf("Failed to enroll using certificate authority: %s", resp.Status) 84 | } 85 | response := map[string]interface{}{} 86 | err = json.NewDecoder(resp.Body).Decode(&response) 87 | if err != nil { 88 | return nil, err 89 | } 90 | success, ok := response["success"].(bool) 91 | if !success || !ok { 92 | return nil, errors.Errorf("Failed to enroll using certificate authority: %v", response) 93 | } 94 | result, ok := response["result"].(map[string]interface{}) 95 | if !ok { 96 | return nil, errors.Errorf("Invalid response to enroll using certificate authority: %v", response) 97 | } 98 | b64cert, ok := result["Cert"].(string) 99 | if !ok { 100 | return nil, errors.Errorf("Invalid response to enroll using certificate authority: %v", response) 101 | } 102 | cert, err := base64.StdEncoding.DecodeString(b64cert) 103 | if err != nil { 104 | return nil, err 105 | } 106 | pk, err := x509.MarshalPKCS8PrivateKey(privateKey) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return identity.FromClient(&client.Identity{ 111 | DisplayName: name, 112 | Certificate: cert, 113 | PrivateKey: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pk}), 114 | CA: c.ca.identity.Certificate().Bytes(), 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # NOTE THIS DOCKERFILE IS CONSIDERED DEPRECATED 5 | FROM registry.access.redhat.com/ubi8/ubi-minimal AS base 6 | ADD docker/couchdb-rpm.repo /etc/yum.repos.d/couchdb-rpm.repo 7 | RUN microdnf install couchdb findutils gcc gcc-c++ git gzip make python3 shadow-utils tar unzip xz \ 8 | && groupadd -g 7051 ibp-user \ 9 | && useradd -u 7051 -g ibp-user -G root -s /bin/bash ibp-user \ 10 | && microdnf remove shadow-utils \ 11 | && microdnf clean all 12 | ADD docker/local.ini /opt/couchdb/etc/local.d/local.ini 13 | ENV PATH=/opt/couchdb/bin:${PATH} 14 | ENV TINI_VERSION v0.19.0 15 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 16 | ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 /usr/local/bin/jq 17 | RUN chmod +x /tini /usr/local/bin/jq 18 | RUN mkdir -p /opt/go /opt/node /opt/java \ 19 | && curl -sSL https://dl.google.com/go/go1.17.2.linux-amd64.tar.gz | tar xzf - -C /opt/go --strip-components=1 \ 20 | && curl -sSL https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.7%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.7_10.tar.gz | tar xzf - -C /opt/java --strip-components=1 \ 21 | && curl -sSL https://nodejs.org/download/release/v16.4.0/node-v16.4.0-linux-x64.tar.xz | tar xJf - -C /opt/node --strip-components=1 22 | ENV GOROOT=/opt/go 23 | ENV GOCACHE=/tmp/gocache 24 | ENV GOENV=/tmp/goenv 25 | ENV GOPATH=/tmp/go 26 | ENV JAVA_HOME=/opt/java 27 | ENV MAVEN_OPTS="-Dmaven.repo.local=/tmp/maven" 28 | ENV npm_config_cache=/tmp/npm-cache 29 | ENV npm_config_devdir=/tmp/npm-devdir 30 | ENV PATH=/opt/go/bin:/opt/node/bin:/opt/java/bin:${PATH} 31 | RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-5.6.4-bin.zip \ 32 | && unzip -qq /tmp/gradle.zip -d /opt \ 33 | && mkdir -p /opt/gradle/bin \ 34 | && cd /opt/gradle/bin \ 35 | && /opt/gradle-5.6.4/bin/gradle wrapper \ 36 | && rm -f /tmp/gradle.zip \ 37 | && rm -rf /opt/gradle-5.6.4 \ 38 | && cd - \ 39 | && curl -sSL https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz | tar xzf - -C /opt \ 40 | && mv /opt/apache-maven-3.6.3 /opt/maven 41 | ENV PATH=/opt/gradle/bin:/opt/maven/bin:${PATH} 42 | ADD builders/java/pom.xml /opt/fabric-chaincode-java/ 43 | RUN mkdir -p /opt/fabric \ 44 | && curl -sSL https://github.com/hyperledger/fabric/releases/download/v2.4.6/hyperledger-fabric-linux-amd64-2.4.6.tar.gz | tar xzf - -C /opt/fabric \ 45 | && curl -sSL https://github.com/hyperledger/fabric-ca/releases/download/v1.5.2/hyperledger-fabric-ca-linux-amd64-1.5.2.tar.gz | tar xzf - -C /opt/fabric \ 46 | && cd /opt/fabric-chaincode-java \ 47 | # && mvn -q dependency:copy-dependencies -DoutputDirectory=/opt/fabric-chaincode-java/lib \ 48 | && npm install --unsafe-perm -g fabric-shim@2.4.2 \ 49 | && rm -rf /tmp/gocache /tmp/goenv /tmp/go /tmp/maven /tmp/npm-cache /tmp/npm-devdir 50 | ENV FABRIC_CFG_PATH=/opt/fabric/config 51 | ENV PATH=/opt/fabric/bin:${PATH} 52 | 53 | FROM base AS builder 54 | ADD . /tmp/microfab 55 | RUN cd /tmp/microfab \ 56 | && mkdir -p /opt/microfab/bin /opt/microfab/data \ 57 | && chgrp -R root /opt/microfab/data \ 58 | && chmod -R g=u /opt/microfab/data \ 59 | && go build -o /opt/microfab/bin/microfabd cmd/microfabd/main.go \ 60 | && cp -rf builders /opt/microfab/builders 61 | 62 | FROM base 63 | COPY --from=builder /opt/microfab /opt/microfab 64 | COPY --from=base /opt/fabric/builders/ccaas /opt/microfab/builders/ccaas 65 | COPY docker/docker-entrypoint.sh / 66 | ENV MICROFAB_HOME=/opt/microfab 67 | ENV PATH=/opt/microfab/bin:${PATH} 68 | EXPOSE 8080 69 | USER 7051 70 | VOLUME /opt/microfab/data 71 | ENTRYPOINT [ "/tini", "--", "/docker-entrypoint.sh" ] 72 | -------------------------------------------------------------------------------- /pkg/microfab/start.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/docker/client" 12 | "github.com/docker/docker/pkg/stdcopy" 13 | "github.com/docker/go-connections/nat" 14 | "github.com/pkg/errors" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | var startCmd = &cobra.Command{ 20 | Use: "start", 21 | Short: "Starts the microfab image running", 22 | GroupID: "mf", 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | return start() 25 | }, 26 | } 27 | 28 | var logs bool 29 | 30 | func init() { 31 | startCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "Force restart if microfab already running") 32 | startCmd.PersistentFlags().BoolVarP(&logs, "logs", "l", false, "Display the logs (docker logs -f microfab)") 33 | 34 | startCmd.PersistentFlags().StringVar(&cfg, "config", defaultCfg, "Microfab config") 35 | startCmd.PersistentFlags().StringVar(&cfgFile, "configFile", "", "Microfab config file") 36 | 37 | startCmd.MarkFlagsMutuallyExclusive("config", "configFile") 38 | 39 | viper.BindPFlag("MICROFAB_CONFIG", rootCmd.PersistentFlags().Lookup("config")) 40 | 41 | } 42 | 43 | func start() error { 44 | ctx := context.Background() 45 | 46 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 47 | if err != nil { 48 | return errors.Wrapf(err, "Unable to create Docker client") 49 | } 50 | defer cli.Close() 51 | var env = make([]string, 2, 200) 52 | 53 | log.Printf("Starting microfab container..\n") 54 | 55 | cfg, err = GetConfig() 56 | if err != nil { 57 | return errors.Wrapf(err, "Unable to determine config") 58 | } 59 | 60 | env[0] = "FABRIC_LOGGING_SPEC=info" 61 | env[1] = fmt.Sprintf("MICROFAB_CONFIG=%s", cfg) 62 | microFabImage := "ghcr.io/hyperledger-labs/microfab:latest" 63 | containername := "microfab" 64 | 65 | // pull down the image if needed 66 | err = DownloadImage(microFabImage) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | // check to see if a container is allready running 72 | running, err := ImageRunning(containername) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | if running { 78 | if force { 79 | if err = Stop(containername); err != nil { 80 | return err 81 | } 82 | } else { 83 | return errors.Errorf("Unable to start '%s' is already running: use --force", containername) 84 | } 85 | 86 | } 87 | // create the configuration for the container, primarily exposing port 8080 88 | config := &container.Config{ 89 | Image: microFabImage, 90 | ExposedPorts: nat.PortSet{"8080": struct{}{}}, 91 | Env: env, 92 | } 93 | 94 | hostConfig := &container.HostConfig{ 95 | PortBindings: map[nat.Port][]nat.PortBinding{nat.Port("8080"): {{HostIP: "127.0.0.1", HostPort: "8080"}}}, 96 | AutoRemove: true, 97 | } 98 | 99 | resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, containername) 100 | if err != nil { 101 | 102 | fmt.Printf("%v %v\n", resp, err) 103 | return errors.Wrapf(err, "Unable to create container") 104 | } 105 | 106 | if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { 107 | return errors.Wrapf(err, "Unable to start contianer") 108 | } 109 | 110 | log.Printf("Container ID %s\n", resp.ID) 111 | log.Printf("Microfab is up and running\n") 112 | if logs { 113 | out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true}) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | stdcopy.StdCopy(os.Stdout, os.Stderr, out) 119 | } 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/pkg/ca/ca.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package ca 6 | 7 | import ( 8 | "fmt" 9 | "net/url" 10 | "os/exec" 11 | "strconv" 12 | 13 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 14 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 15 | ) 16 | 17 | // CA represents a loaded CA definition. 18 | type CA struct { 19 | organization *organization.Organization 20 | identity *identity.Identity 21 | directory string 22 | apiPort int32 23 | apiURL *url.URL 24 | operationsPort int32 25 | operationsURL *url.URL 26 | command *exec.Cmd 27 | tls *identity.Identity 28 | } 29 | 30 | // New creates a new CA. 31 | func New(organization *organization.Organization, directory string, apiPort int32, apiURL string, operationsPort int32, operationsURL string) (*CA, error) { 32 | identity := organization.CA() 33 | parsedAPIURL, err := url.Parse(apiURL) 34 | if err != nil { 35 | return nil, err 36 | } 37 | parsedOperationsURL, err := url.Parse(operationsURL) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &CA{organization, identity, directory, apiPort, parsedAPIURL, operationsPort, parsedOperationsURL, nil, nil}, nil 42 | } 43 | 44 | // TLS gets the TLS identity for this CA. 45 | func (c *CA) TLS() *identity.Identity { 46 | return c.tls 47 | } 48 | 49 | // EnableTLS enables TLS for this CA. 50 | func (c *CA) EnableTLS(tls *identity.Identity) { 51 | c.tls = tls 52 | } 53 | 54 | // Organization returns the organization of the CA. 55 | func (c *CA) Organization() *organization.Organization { 56 | return c.organization 57 | } 58 | 59 | // APIHostname returns the hostname of the CA. 60 | func (c *CA) APIHostname(internal bool) string { 61 | if internal { 62 | return "localhost" 63 | } 64 | return c.apiURL.Hostname() 65 | } 66 | 67 | // APIHost returns the host (hostname:port) of the CA. 68 | func (c *CA) APIHost(internal bool) string { 69 | if internal { 70 | return fmt.Sprintf("localhost:%d", c.apiPort) 71 | } 72 | return c.apiURL.Host 73 | } 74 | 75 | // APIPort returns the API port of the CA. 76 | func (c *CA) APIPort(internal bool) int32 { 77 | if internal { 78 | return c.apiPort 79 | } 80 | port, _ := strconv.Atoi(c.apiURL.Port()) 81 | return int32(port) 82 | } 83 | 84 | // APIURL returns the API URL of the CA. 85 | func (c *CA) APIURL(internal bool) *url.URL { 86 | scheme := "http" 87 | if c.tls != nil { 88 | scheme = "https" 89 | } 90 | if internal { 91 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, c.apiPort)) 92 | return url 93 | } 94 | return c.apiURL 95 | } 96 | 97 | // OperationsHostname returns the hostname of the CA. 98 | func (c *CA) OperationsHostname(internal bool) string { 99 | if internal { 100 | return "localhost" 101 | } 102 | return c.operationsURL.Hostname() 103 | } 104 | 105 | // OperationsHost returns the host (hostname:port) of the CA. 106 | func (c *CA) OperationsHost(internal bool) string { 107 | if internal { 108 | return fmt.Sprintf("localhost:%d", c.operationsPort) 109 | } 110 | return c.operationsURL.Host 111 | } 112 | 113 | // OperationsPort returns the operations port of the CA. 114 | func (c *CA) OperationsPort(internal bool) int32 { 115 | if internal { 116 | return c.operationsPort 117 | } 118 | port, _ := strconv.Atoi(c.operationsURL.Port()) 119 | return int32(port) 120 | } 121 | 122 | // OperationsURL returns the operations URL of the CA. 123 | func (c *CA) OperationsURL(internal bool) *url.URL { 124 | scheme := "http" 125 | if c.tls != nil { 126 | scheme = "https" 127 | } 128 | if internal { 129 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, c.operationsPort)) 130 | return url 131 | } 132 | return c.operationsURL 133 | } 134 | -------------------------------------------------------------------------------- /pkg/microfab/connect.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/url" 8 | "os" 9 | "path" 10 | 11 | "github.com/hyperledger-labs/microfab/pkg/client" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var connectCmd = &cobra.Command{ 17 | Use: "connect", 18 | Short: "Writes out connection details for use by the Peer CLI and SDKs", 19 | GroupID: "mf", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | return connect() 22 | }, 23 | } 24 | 25 | func init() { 26 | connectCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "Force overwriting details directory") 27 | connectCmd.PersistentFlags().StringVar(&mspdir, "msp", "_mfcfg", "msp output directory") 28 | } 29 | 30 | func connect() error { 31 | 32 | urlStr := "http://console.127-0-0-1.nip.io:8080" 33 | testURL, err := url.Parse(urlStr) 34 | if err != nil { 35 | return errors.Wrapf(err, "Unable to parse URL") 36 | } 37 | 38 | rootDir := path.Clean(mspdir) 39 | 40 | log.Printf("Connecting to URL '%s'\n", urlStr) 41 | log.Printf("Identity and Configuration '%s'\n", rootDir) 42 | 43 | // check to see if the directory exists, and if it does emptry 44 | cfgExists, err := Exists(rootDir) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | if cfgExists { 50 | empty, err := isEmpty(rootDir) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if !empty && !force { 56 | return errors.Errorf("Config directory '%s' is not empty, use --force to overwrite", rootDir) 57 | } 58 | } else { 59 | os.MkdirAll(rootDir, 0755) 60 | } 61 | 62 | mfc, err := client.New(testURL, false) 63 | if err != nil { 64 | return errors.Wrapf(err, "Unable to create client") 65 | } 66 | 67 | orgs, err := mfc.GetOrganizations() 68 | if err != nil { 69 | return errors.Wrapf(err, "Unable to get Organizations") 70 | } 71 | 72 | for _, org := range orgs { 73 | 74 | id, err := mfc.GetIdentity(org) 75 | if err != nil { 76 | return errors.Wrapf(err, "Unable to get Identity") 77 | } 78 | 79 | idRoot := path.Join(rootDir, org, id.ID, "msp") 80 | os.MkdirAll(path.Join(idRoot, "admincerts"), 0755) 81 | os.MkdirAll(path.Join(idRoot, "cacerts"), 0755) 82 | os.MkdirAll(path.Join(idRoot, "keystore"), 0755) 83 | os.MkdirAll(path.Join(idRoot, "signcerts"), 0755) 84 | 85 | os.WriteFile(path.Join(idRoot, "admincerts", "cert.pem"), id.Certificate, 0644) 86 | os.WriteFile(path.Join(idRoot, "signcerts", "cert.pem"), id.Certificate, 0644) 87 | os.WriteFile(path.Join(idRoot, "keystore", "cert_sk"), id.PrivateKey, 0644) 88 | os.WriteFile(path.Join(idRoot, "cacerts", "cert.pem"), id.CA, 0644) 89 | 90 | // get the peers, if there's no peer then move on 91 | peer, err := mfc.GetPeer(org) 92 | if err != nil { 93 | continue 94 | } 95 | 96 | u, err := url.Parse(peer.APIURL) 97 | if err != nil { 98 | return errors.Wrapf(err, "Unable to prase APIURL") 99 | } 100 | 101 | f, err := os.Create(path.Join(rootDir, fmt.Sprintf("%s.env", org))) 102 | if err != nil { 103 | return errors.Wrapf(err, "Unable to form path for context") 104 | } 105 | 106 | f.WriteString(fmt.Sprintf("export CORE_PEER_ADDRESS=%s\n", u.Host)) 107 | f.WriteString(fmt.Sprintf("export CORE_PEER_LOCALMSPID=%s\n", peer.MSPID)) 108 | f.WriteString(fmt.Sprintf("export CORE_PEER_MSPCONFIGPATH=%s\n", idRoot)) 109 | f.Sync() 110 | 111 | log.Printf("For %s context run 'source %s'", org, f.Name()) 112 | 113 | } 114 | return nil 115 | 116 | } 117 | 118 | func isEmpty(name string) (bool, error) { 119 | f, err := os.Open(name) 120 | if err != nil { 121 | return false, err 122 | } 123 | defer f.Close() 124 | 125 | _, err = f.Readdirnames(1) // Or f.Readdir(1) 126 | if err == io.EOF { 127 | return true, nil 128 | } 129 | return false, err // Either not empty or error, suits both cases 130 | } 131 | -------------------------------------------------------------------------------- /pkg/microfab/util.go: -------------------------------------------------------------------------------- 1 | package microfab 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path" 11 | "strings" 12 | 13 | "github.com/docker/docker/api/types" 14 | "github.com/docker/docker/client" 15 | "github.com/pkg/errors" 16 | "github.com/spf13/viper" 17 | "golang.org/x/exp/slices" 18 | ) 19 | 20 | // PullStatus of the JSON structure returned by docker 21 | type PullStatus struct { 22 | Status string `json:"status"` 23 | } 24 | 25 | // DownloadImage pulls the image if needed 26 | func DownloadImage(microFabImage string) error { 27 | ctx := context.Background() 28 | log.Printf("Checking for any image already") 29 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 30 | if err != nil { 31 | return errors.Wrapf(err, "Unable to create Docker client") 32 | } 33 | defer cli.Close() 34 | 35 | images, err := cli.ImageList(ctx, types.ImageListOptions{}) 36 | if err != nil { 37 | return errors.Wrapf(err, "Unable to list images") 38 | } 39 | 40 | found := false 41 | for _, image := range images { 42 | found = slices.Contains(image.RepoTags, microFabImage) 43 | if found { 44 | break 45 | } 46 | } 47 | 48 | if !found { 49 | log.Printf("Pulling image %s", microFabImage) 50 | out, err := cli.ImagePull(ctx, microFabImage, types.ImagePullOptions{}) 51 | if err != nil { 52 | return errors.Wrapf(err, "Unable to pull images") 53 | } 54 | 55 | defer out.Close() 56 | 57 | // rather inelegant way of getting status - effectively scanning 58 | // the stdout of the command 59 | buf := bufio.NewScanner(out) 60 | for buf.Scan() { 61 | var s PullStatus 62 | json.Unmarshal(buf.Bytes(), &s) 63 | if strings.HasPrefix(s.Status, "Status: Downloaded") { 64 | log.Printf(s.Status) 65 | } 66 | 67 | } 68 | } else { 69 | log.Printf("Found image %s", microFabImage) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // ImageRunning determines if the image is already running 76 | func ImageRunning(containerName string) (bool, error) { 77 | ctx := context.Background() 78 | log.Printf("Checking if '%s' already running", containerName) 79 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 80 | if err != nil { 81 | return false, errors.Wrapf(err, "Unable to create Docker client") 82 | } 83 | defer cli.Close() 84 | 85 | containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) 86 | if err != nil { 87 | return false, errors.Wrapf(err, "Unable to list containers") 88 | } 89 | 90 | // not sure about why docker adds the prefix / 91 | cn := fmt.Sprintf("/%s", containerName) 92 | 93 | // check the list of names to see if it is there 94 | // assume it is running if it's present 95 | found := false 96 | for _, container := range containers { 97 | found = slices.Contains(container.Names, cn) 98 | if found { 99 | break 100 | } 101 | } 102 | 103 | return found, nil 104 | 105 | } 106 | 107 | // GetConfig resolves the microfab configuration 108 | func GetConfig() (string, error) { 109 | cfg = viper.GetString("MICROFAB_CONFIG") 110 | if cfg == "" { 111 | cf := path.Clean(cfgFile) 112 | exist, err := Exists(cf) 113 | if err != nil { 114 | return "", err 115 | } 116 | 117 | if exist { 118 | cfgData, err := os.ReadFile(cf) 119 | if err != nil { 120 | return "", err 121 | } 122 | cfg = string(cfgData) 123 | } else { 124 | return "", errors.Errorf("Unable to locate config from file, envvar or cli option") 125 | } 126 | 127 | } 128 | return cfg, nil 129 | } 130 | 131 | // Exists returns whether the given file or directory exists 132 | func Exists(path string) (bool, error) { 133 | _, err := os.Stat(path) 134 | if err == nil { 135 | return true, nil 136 | } 137 | if os.IsNotExist(err) { 138 | return false, nil 139 | } 140 | return false, err 141 | } 142 | -------------------------------------------------------------------------------- /examples/justfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright IBM 2022 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | # Main justfile to run all the development scripts 8 | # To install 'just' see https://github.com/casey/just#installation 9 | 10 | 11 | # Ensure all properties are exported as shell env-vars 12 | set export 13 | 14 | # set the current directory, and the location of the test dats 15 | CWDIR := justfile_directory() 16 | 17 | _default: 18 | @just -f {{justfile()}} --list 19 | 20 | # Removes running Micorfab, and cleans the _cfg direcotyr 21 | clean: 22 | #!/bin/bash 23 | set -euo pipefail 24 | 25 | export CFG=$CWDIR/../_cfg 26 | rm -rf "${CFG}" || mkdir -p "${CFG}" 27 | mkdir -p "${CFG}/data" 28 | 29 | if docker inspect microfab &>/dev/null; then 30 | echo "Removing existing microfab container:" 31 | docker kill microfab 32 | fi 33 | 34 | # Run a TLS enabled microfab; simple two org/one channel config 35 | tls-mf: clean 36 | #!/bin/bash 37 | set -euo pipefail 38 | 39 | export MICROFAB_CONFIG='{ 40 | "couchdb":false, 41 | "endorsing_organizations":[ 42 | { 43 | "name": "org1" 44 | }, 45 | { 46 | "name":"org2" 47 | } 48 | 49 | ], 50 | "channels":[ 51 | { 52 | "name": "ch-a", 53 | "endorsing_organizations":[ 54 | "org1","org2" 55 | ] 56 | } 57 | ], 58 | "tls": { 59 | "enabled":true 60 | }, 61 | "capability_level":"V2_5" 62 | }' 63 | 64 | docker run -d --name microfab -p 8080:8080 --add-host host.docker.internal:host-gateway \ 65 | --rm -e MICROFAB_CONFIG="${MICROFAB_CONFIG}" \ 66 | -e FABRIC_LOGGING_SPEC=info \ 67 | tls/microfab 68 | 69 | # Get the configuration and extract the information 70 | sleep 10 71 | 72 | export CFG=$CWDIR/../_cfg 73 | 74 | curl -sSL --insecure https://console.localho.st:8080/ak/api/v1/components > $CFG/cfg.json 75 | weft microfab -w $CFG/_wallets -p $CFG/_gateways -m $CFG/_msp -f --config $CFG/cfg.json 76 | 77 | 78 | registerEnrollUser: 79 | #!/bin/bash 80 | set -xeuo pipefail 81 | 82 | # ID under Org1 83 | 84 | fabric-ca-client register --debug \ 85 | --id.name owner \ 86 | --id.secret ownerpw \ 87 | --id.type client \ 88 | --url https://org1ca-api.127-0-0-1.nip.io:8080 \ 89 | --tls.certfiles $CWDIR/../_cfg/_msp/tls/org1peer/tlsca-org1peer-cert.pem \ 90 | --mspdir $CWDIR/../_cfg/_msp/org1/org1caadmin/msp 91 | 92 | 93 | fabric-ca-client enroll -u https://owner:ownerpw@org1ca-api.127-0-0-1.nip.io:8080 \ 94 | --caname org1ca \ 95 | -M $CWDIR/../_cfg/_msp/org1/org1fred \ 96 | --tls.certfiles $CWDIR/../_cfg/_msp/tls/org1peer/tlsca-org1peer-cert.pem 97 | 98 | # ID under ORG2 99 | 100 | fabric-ca-client register --debug \ 101 | --id.name buyer \ 102 | --id.secret buyerpw \ 103 | --id.type client \ 104 | --url https://org2ca-api.127-0-0-1.nip.io:8080 \ 105 | --tls.certfiles $CWDIR/../_cfg/_msp/tls/org2peer/tlsca-org1peer-cert.pem \ 106 | --mspdir $CWDIR/../_cfg/_msp/org2/org2caadmin/msp 107 | 108 | 109 | fabric-ca-client enroll -u https://buyer:buyerpw@org2ca-api.127-0-0-1.nip.io:8080 \ 110 | --caname org2ca \ 111 | -M $CWDIR/../_cfg/_msp/org2/org2buyer \ 112 | --tls.certfiles $CWDIR/../_cfg/_msp/tls/org2peer/tlsca-org2peer-cert.pem 113 | -------------------------------------------------------------------------------- /internal/pkg/blocks/blocks.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package blocks 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/protoutil" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 14 | "github.com/hyperledger/fabric-protos-go/common" 15 | "github.com/hyperledger/fabric-protos-go/orderer" 16 | ) 17 | 18 | // DeliverCallback is a callback function called for every block returned by Deliver. 19 | type DeliverCallback func(*common.Block) error 20 | 21 | // Deliverer can be implemented by types that can deliver one or more blocks. 22 | type Deliverer interface { 23 | MSPID() string 24 | Identity() *identity.Identity 25 | Deliver(envelope *common.Envelope, callback DeliverCallback) error 26 | } 27 | 28 | // GetConfigBlock gets the latest config block from the specified channel. 29 | func GetConfigBlock(deliverer Deliverer, channel string) (*common.Block, error) { 30 | newestBlock, err := GetNewestBlock(deliverer, channel) 31 | if err != nil { 32 | return nil, err 33 | } 34 | metadataBytes := newestBlock.GetMetadata().GetMetadata()[common.BlockMetadataIndex_LAST_CONFIG] 35 | metadata := &common.Metadata{} 36 | util.UnmarshalOrPanic(metadataBytes, metadata) 37 | lastConfig := &common.LastConfig{} 38 | util.UnmarshalOrPanic(metadata.Value, lastConfig) 39 | return GetSpecificBlock(deliverer, channel, lastConfig.Index) 40 | } 41 | 42 | // GetNewestBlock gets the newest block from the specified channel. 43 | func GetNewestBlock(deliverer Deliverer, channel string) (*common.Block, error) { 44 | seekInfo := &orderer.SeekInfo{ 45 | Start: &orderer.SeekPosition{ 46 | Type: &orderer.SeekPosition_Newest{ 47 | Newest: &orderer.SeekNewest{}, 48 | }, 49 | }, 50 | Stop: &orderer.SeekPosition{ 51 | Type: &orderer.SeekPosition_Newest{ 52 | Newest: &orderer.SeekNewest{}, 53 | }, 54 | }, 55 | Behavior: orderer.SeekInfo_BLOCK_UNTIL_READY, 56 | } 57 | return getBlock(deliverer, channel, seekInfo) 58 | } 59 | 60 | // GetSpecificBlock gets the specified block from the specified channel. 61 | func GetSpecificBlock(deliverer Deliverer, channel string, number uint64) (*common.Block, error) { 62 | seekInfo := &orderer.SeekInfo{ 63 | Start: &orderer.SeekPosition{ 64 | Type: &orderer.SeekPosition_Specified{ 65 | Specified: &orderer.SeekSpecified{ 66 | Number: number, 67 | }, 68 | }, 69 | }, 70 | Stop: &orderer.SeekPosition{ 71 | Type: &orderer.SeekPosition_Specified{ 72 | Specified: &orderer.SeekSpecified{ 73 | Number: number, 74 | }, 75 | }, 76 | }, 77 | Behavior: orderer.SeekInfo_BLOCK_UNTIL_READY, 78 | } 79 | return getBlock(deliverer, channel, seekInfo) 80 | } 81 | 82 | // GetGenesisBlock gets the genesis block from the specified channel. 83 | func GetGenesisBlock(deliverer Deliverer, channel string) (*common.Block, error) { 84 | return GetSpecificBlock(deliverer, channel, 0) 85 | } 86 | 87 | func buildEnvelope(deliverer Deliverer, channel string, seekInfo *orderer.SeekInfo) *common.Envelope { 88 | txID := txid.New(deliverer.MSPID(), deliverer.Identity()) 89 | header := protoutil.BuildHeader(common.HeaderType_DELIVER_SEEK_INFO, channel, txID) 90 | payload := protoutil.BuildPayload(header, seekInfo) 91 | return protoutil.BuildEnvelope(payload, deliverer.Identity()) 92 | } 93 | 94 | func getBlock(deliverer Deliverer, channel string, seekInfo *orderer.SeekInfo) (*common.Block, error) { 95 | envelope := buildEnvelope(deliverer, channel, seekInfo) 96 | var result *common.Block 97 | err := deliverer.Deliver(envelope, func(block *common.Block) error { 98 | if result != nil { 99 | return errors.New("Multiple blocks returned by seek info request") 100 | } 101 | result = block 102 | return nil 103 | }) 104 | if err != nil { 105 | return nil, err 106 | } else if result == nil { 107 | return nil, errors.New("No blocks returned by seek info request") 108 | } 109 | return result, nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/pkg/peer/peer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer_test 6 | 7 | import ( 8 | "io/ioutil" 9 | 10 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/peer" 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("the peer package", func() { 17 | 18 | var testOrganization *organization.Organization 19 | var testDirectory string 20 | 21 | BeforeEach(func() { 22 | var err error 23 | testOrganization, err = organization.New("Org1", nil, nil) 24 | Expect(err).NotTo(HaveOccurred()) 25 | testDirectory, err = ioutil.TempDir("", "ut-peer") 26 | Expect(err).NotTo(HaveOccurred()) 27 | }) 28 | 29 | Context("peer.New()", func() { 30 | 31 | When("called", func() { 32 | It("creates a new peer", func() { 33 | p, err := peer.New(testOrganization, testDirectory, 8080, 7051, "grpc://org1peer-api.127-0-0-1.nip.io:8080", 7052, "grpc://org1peer-chaincode.127-0-0-1.nip.io:8080", 8443, "http://org1peer-operations.127-0-0-1.nip.io:8080", false, 0, 4000, "http://org1peer-gossip.127-0-0-1.nip.io:4000") 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(p.Organization()).To(Equal(testOrganization)) 36 | Expect(p.MSPID()).To(Equal(testOrganization.MSPID())) 37 | Expect(p.APIHostname(false)).To(Equal("org1peer-api.127-0-0-1.nip.io")) 38 | Expect(p.APIHostname(true)).To(Equal("localhost")) 39 | Expect(p.APIHost(false)).To(Equal("org1peer-api.127-0-0-1.nip.io:8080")) 40 | Expect(p.APIHost(true)).To(Equal("localhost:7051")) 41 | Expect(p.APIPort(false)).To(BeEquivalentTo(8080)) 42 | Expect(p.APIPort(true)).To(BeEquivalentTo(7051)) 43 | Expect(p.APIURL(false).String()).To(BeEquivalentTo("grpc://org1peer-api.127-0-0-1.nip.io:8080")) 44 | Expect(p.APIURL(true).String()).To(BeEquivalentTo("grpc://localhost:7051")) 45 | Expect(p.ChaincodeHost(false)).To(Equal("org1peer-chaincode.127-0-0-1.nip.io:8080")) 46 | Expect(p.ChaincodeHost(true)).To(Equal("localhost:7052")) 47 | Expect(p.ChaincodePort(false)).To(BeEquivalentTo(8080)) 48 | Expect(p.ChaincodePort(true)).To(BeEquivalentTo(7052)) 49 | Expect(p.ChaincodeURL(false).String()).To(BeEquivalentTo("grpc://org1peer-chaincode.127-0-0-1.nip.io:8080")) 50 | Expect(p.ChaincodeURL(true).String()).To(BeEquivalentTo("grpc://localhost:7052")) 51 | Expect(p.OperationsHost(false)).To(Equal("org1peer-operations.127-0-0-1.nip.io:8080")) 52 | Expect(p.OperationsHost(true)).To(Equal("localhost:8443")) 53 | Expect(p.OperationsPort(false)).To(BeEquivalentTo(8080)) 54 | Expect(p.OperationsPort(true)).To(BeEquivalentTo(8443)) 55 | Expect(p.OperationsURL(false).String()).To(BeEquivalentTo("http://org1peer-operations.127-0-0-1.nip.io:8080")) 56 | Expect(p.OperationsURL(true).String()).To(BeEquivalentTo("http://localhost:8443")) 57 | }) 58 | }) 59 | 60 | When("called with an invalid API URL", func() { 61 | It("returns an error", func() { 62 | _, err := peer.New(testOrganization, testDirectory, 8080, 7051, "!@£$%^&*()_+", 7052, "grpc://org1peer-chaincode.127-0-0-1.nip.io:8080", 8443, "http://org1peer-operations.127-0-0-1.nip.io:8080", false, 0, 4000, "http://org1peer-gossip.127.0.0.1.nip.io") 63 | Expect(err).To(HaveOccurred()) 64 | }) 65 | }) 66 | 67 | When("called with an invalid chaincode URL", func() { 68 | It("returns an error", func() { 69 | _, err := peer.New(testOrganization, testDirectory, 8080, 7051, "grpc://org1peer-api.127-0-0-1.nip.io:8080", 7052, "!@£$%^&*()_+", 8443, "http://org1peer-operations.127-0-0-1.nip.io:8080", false, 0, 4000, "http://org1peer-gossip.127.0.0.1.nip.io") 70 | Expect(err).To(HaveOccurred()) 71 | }) 72 | }) 73 | 74 | When("called with an invalid operations URL", func() { 75 | It("returns an error", func() { 76 | _, err := peer.New(testOrganization, testDirectory, 8080, 7051, "grpc://org1peer-api.127-0-0-1.nip.io:8080", 7052, "grpc://org1peer-chaincode.127-0-0-1.nip.io:8080", 8443, "!@£$%^&*()_+", false, 0, 4000, "http://org1peer-gossip.127.0.0.1.nip.io") 77 | Expect(err).To(HaveOccurred()) 78 | }) 79 | }) 80 | 81 | }) 82 | 83 | }) 84 | -------------------------------------------------------------------------------- /docs/ConfiguringMicrofab.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | You can configure the 'topology' that microfab creates, and also some limited configuration of the Fabric components. 3 | ## Configuring microfab 4 | 5 | Microfab can be configured by specifying the `MICROFAB_CONFIG` environment variable. For example, to start Microfab with different organizations using Docker, run the following commands: 6 | 7 | export MICROFAB_CONFIG='{ 8 | "endorsing_organizations":[ 9 | { 10 | "name": "SampleOrg" 11 | } 12 | ], 13 | "channels":[ 14 | { 15 | "name": "mychannel", 16 | "endorsing_organizations":[ 17 | "SampleOrg" 18 | ] 19 | } 20 | ] 21 | }' 22 | 23 | docker run -p 8080:8080 -e MICROFAB_CONFIG ibmcom/ibp-microfab 24 | 25 | 26 | 27 | The configuration is a JSON object with the following keys: 28 | 29 | - `domain` 30 | 31 | The domain name to use. The domain name must be resolvable both outside and inside the container, and it must resolve to an IP address of that container (or the system hosting the container). 32 | 33 | Default value: `"127-0-0-1.nip.io"` 34 | 35 | - `port` 36 | 37 | The port to use. The port must be accessible both outside and inside the container. 38 | 39 | Default value: `8080` 40 | 41 | - `directory` 42 | 43 | The directory to store data in within the container. 44 | 45 | Default value: `"/home/microfab/data"` 46 | 47 | It is possible to map this externally as a volume mount; however often you can run into issues with the user inside the docker container having a different `id` to the host user. It is suggested that _do not_ volume mount this. 48 | 49 | - `ordering_organization` 50 | 51 | The ordering organization. 52 | 53 | Default value: 54 | 55 | { 56 | "name": "Orderer" // The name of the organization. 57 | } 58 | 59 | - `endorsing_organizations` 60 | 61 | The list of endorsing organizations. 62 | 63 | Default value: 64 | 65 | [ 66 | { 67 | "name": "Org1" // The name of the organization. 68 | } 69 | ] 70 | 71 | - `channels` 72 | 73 | The list of channels. 74 | 75 | Default value: 76 | 77 | [ 78 | { 79 | "name": "channel1", // The name of the channel. 80 | "endorsing_organizations": [ // The list of endorsing organizations that are members of the channel. 81 | "Org1" 82 | ], 83 | "capability_level": "V2_5" // Optional: the application capability level of the channel. 84 | } 85 | ] 86 | 87 | - `capability_level` 88 | 89 | The application capability level of all channels. Can be overriden on a per-channel basis. 90 | 91 | Default value: `"V2_5"` 92 | 93 | - `couchdb` 94 | 95 | Whether or not to use CouchDB as the world state database. 96 | 97 | Default value: `true` 98 | 99 | - `certificate_authorities` 100 | 101 | Whether or not to create certificate authorities for all endorsing organizations. 102 | 103 | Default value: `true` 104 | 105 | - `timeout` 106 | 107 | The time to wait for all components to start. 108 | 109 | Default value: `"30s"` 110 | 111 | - `tls` 112 | 113 | The TLS configuration. 114 | 115 | Default value: 116 | 117 | { 118 | "enabled": false, // Set to true to enable TLS. 119 | "certificate": null, // Optional: the TLS certificate to be used. 120 | "private_key": null, // Optional: the TLS private key to be used. 121 | "ca": null // Optional: the TLS CA certificate to be used. 122 | } 123 | 124 | ### Examples 125 | 126 | Configuration example for enabling TLS: 127 | 128 | export MICROFAB_CONFIG='{ 129 | "port": 8443, 130 | "tls": { 131 | "enabled": true 132 | } 133 | }' 134 | 135 | docker run -p 8443:8443 -e MICROFAB_CONFIG ibmcom/ibp-microfab 136 | 137 | 138 | ## Configuring Fabric components 139 | 140 | To alter the logging level of the Fabric Components, add ` -e FABRIC_LOGGING_SPEC=info` to the docker run command. Any other environment variables set will be inheritted by the Fabric Components. 141 | 142 | -------------------------------------------------------------------------------- /internal/pkg/peer/channel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/golang/protobuf/proto" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/protoutil" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 14 | "github.com/hyperledger/fabric-protos-go/common" 15 | "github.com/hyperledger/fabric-protos-go/peer" 16 | ) 17 | 18 | // JoinChannel asks the peer to join the specified channel. 19 | func (c *Connection) JoinChannel(block *common.Block) error { 20 | txID := txid.New(c.mspID, c.identity) 21 | channelHeader := protoutil.BuildChannelHeader(common.HeaderType_CONFIG, "", txID) 22 | cche := &peer.ChaincodeHeaderExtension{ 23 | ChaincodeId: &peer.ChaincodeID{ 24 | Name: "cscc", 25 | }, 26 | } 27 | channelHeader.Extension = util.MarshalOrPanic(cche) 28 | signatureHeader := protoutil.BuildSignatureHeader(txID) 29 | header := &common.Header{ 30 | ChannelHeader: util.MarshalOrPanic(channelHeader), 31 | SignatureHeader: util.MarshalOrPanic(signatureHeader), 32 | } 33 | cciSpec := &peer.ChaincodeInvocationSpec{ 34 | ChaincodeSpec: &peer.ChaincodeSpec{ 35 | Type: peer.ChaincodeSpec_GOLANG, 36 | ChaincodeId: &peer.ChaincodeID{ 37 | Name: "cscc", 38 | }, 39 | Input: &peer.ChaincodeInput{ 40 | Args: [][]byte{ 41 | []byte("JoinChain"), 42 | util.MarshalOrPanic(block), 43 | }, 44 | }, 45 | }, 46 | } 47 | ccpp := &peer.ChaincodeProposalPayload{ 48 | Input: util.MarshalOrPanic(cciSpec), 49 | } 50 | proposal := &peer.Proposal{ 51 | Header: util.MarshalOrPanic(header), 52 | Payload: util.MarshalOrPanic(ccpp), 53 | } 54 | proposalBytes := util.MarshalOrPanic(proposal) 55 | signature := c.identity.Sign(proposalBytes) 56 | signedProposal := &peer.SignedProposal{ 57 | ProposalBytes: proposalBytes, 58 | Signature: signature, 59 | } 60 | response, err := c.ProcessProposal(signedProposal) 61 | if err != nil { 62 | return err 63 | } else if response.Response.Status != int32(common.Status_SUCCESS) { 64 | return fmt.Errorf("Bad proposal response: status %d, mesage %s", response.Response.Status, response.Response.Message) 65 | } 66 | return nil 67 | } 68 | 69 | // ListChannels asks the peer for the list of channels it has joined. 70 | func (c *Connection) ListChannels() ([]string, error) { 71 | txID := txid.New(c.mspID, c.identity) 72 | channelHeader := protoutil.BuildChannelHeader(common.HeaderType_CONFIG, "", txID) 73 | cche := &peer.ChaincodeHeaderExtension{ 74 | ChaincodeId: &peer.ChaincodeID{ 75 | Name: "cscc", 76 | }, 77 | } 78 | channelHeader.Extension = util.MarshalOrPanic(cche) 79 | signatureHeader := protoutil.BuildSignatureHeader(txID) 80 | header := &common.Header{ 81 | ChannelHeader: util.MarshalOrPanic(channelHeader), 82 | SignatureHeader: util.MarshalOrPanic(signatureHeader), 83 | } 84 | cciSpec := &peer.ChaincodeInvocationSpec{ 85 | ChaincodeSpec: &peer.ChaincodeSpec{ 86 | Type: peer.ChaincodeSpec_GOLANG, 87 | ChaincodeId: &peer.ChaincodeID{ 88 | Name: "cscc", 89 | }, 90 | Input: &peer.ChaincodeInput{ 91 | Args: [][]byte{ 92 | []byte("GetChannels"), 93 | }, 94 | }, 95 | }, 96 | } 97 | ccpp := &peer.ChaincodeProposalPayload{ 98 | Input: util.MarshalOrPanic(cciSpec), 99 | } 100 | proposal := &peer.Proposal{ 101 | Header: util.MarshalOrPanic(header), 102 | Payload: util.MarshalOrPanic(ccpp), 103 | } 104 | proposalBytes := util.MarshalOrPanic(proposal) 105 | signature := c.identity.Sign(proposalBytes) 106 | signedProposal := &peer.SignedProposal{ 107 | ProposalBytes: proposalBytes, 108 | Signature: signature, 109 | } 110 | response, err := c.ProcessProposal(signedProposal) 111 | if err != nil { 112 | return nil, err 113 | } else if response.Response.Status != int32(common.Status_SUCCESS) { 114 | return nil, fmt.Errorf("Bad proposal response: status %d, mesage %s", response.Response.Status, response.Response.Message) 115 | } 116 | channelQueryResponse := &peer.ChannelQueryResponse{} 117 | err = proto.Unmarshal(response.Response.Payload, channelQueryResponse) 118 | if err != nil { 119 | return nil, err 120 | } 121 | result := []string{} 122 | for _, channel := range channelQueryResponse.Channels { 123 | result = append(result, channel.ChannelId) 124 | } 125 | return result, nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/pkg/ca/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package ca 6 | 7 | import ( 8 | "bufio" 9 | "crypto/tls" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "path" 17 | "path/filepath" 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | // Start starts the peer. 26 | func (c *CA) Start(timeout time.Duration) error { 27 | logsDirectory := filepath.Join(c.directory, "logs") 28 | if err := os.MkdirAll(logsDirectory, 0755); err != nil { 29 | return err 30 | } 31 | certfile := filepath.Join(c.directory, "ca-cert.pem") 32 | keyfile := filepath.Join(c.directory, "ca-key.pem") 33 | if err := ioutil.WriteFile(certfile, c.identity.Certificate().Bytes(), 0644); err != nil { 34 | return err 35 | } 36 | if err := ioutil.WriteFile(keyfile, c.identity.PrivateKey().Bytes(), 0644); err != nil { 37 | return err 38 | } 39 | args := []string{ 40 | "start", 41 | "--boot", 42 | "admin:adminpw", 43 | "--ca.certfile", 44 | certfile, 45 | "--ca.keyfile", 46 | keyfile, 47 | "--port", 48 | strconv.Itoa(int(c.apiPort)), 49 | "--operations.listenaddress", 50 | fmt.Sprintf("0.0.0.0:%d", c.operationsPort), 51 | "--ca.name", 52 | fmt.Sprintf("%sca", strings.ToLower(c.organization.Name())), 53 | } 54 | if c.tls != nil { 55 | tlsDirectory := path.Join(c.directory, "tls") 56 | if err := os.MkdirAll(tlsDirectory, 0755); err != nil { 57 | return err 58 | } 59 | certFile := path.Join(tlsDirectory, "cert.pem") 60 | keyFile := path.Join(tlsDirectory, "key.pem") 61 | args = append(args, 62 | "--tls.enabled", 63 | "--tls.certfile", 64 | certFile, 65 | "--tls.keyfile", 66 | keyFile, 67 | "--operations.tls.enabled", 68 | "--operations.tls.certfile", 69 | certFile, 70 | "--operations.tls.keyfile", 71 | keyFile, 72 | ) 73 | if err := ioutil.WriteFile(certFile, c.tls.Certificate().Bytes(), 0644); err != nil { 74 | return err 75 | } 76 | if err := ioutil.WriteFile(keyFile, c.tls.PrivateKey().Bytes(), 0644); err != nil { 77 | return err 78 | } 79 | } 80 | fmt.Print(args) 81 | cmd := exec.Command( 82 | "fabric-ca-server", 83 | args..., 84 | ) 85 | cmd.Dir = c.directory 86 | cmd.Env = os.Environ() 87 | cmd.Stdin = nil 88 | logFile, err := os.OpenFile(path.Join(logsDirectory, "ca.log"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 89 | if err != nil { 90 | return err 91 | } 92 | pipe, err := cmd.StdoutPipe() 93 | if err != nil { 94 | return err 95 | } 96 | go func() { 97 | reader := bufio.NewReader(pipe) 98 | scanner := bufio.NewScanner(reader) 99 | scanner.Split(bufio.ScanLines) 100 | id := strings.ToLower(c.identity.Name()) 101 | id = strings.ReplaceAll(id, " ", "") 102 | logger := log.New(os.Stdout, fmt.Sprintf("[%16s] ", id), 0) 103 | for scanner.Scan() { 104 | logger.Println(scanner.Text()) 105 | logFile.WriteString(scanner.Text()) 106 | } 107 | pipe.Close() 108 | logFile.Close() 109 | }() 110 | cmd.Stderr = cmd.Stdout 111 | err = cmd.Start() 112 | if err != nil { 113 | return err 114 | } 115 | c.command = cmd 116 | errchan := make(chan error, 1) 117 | go func() { 118 | err = cmd.Wait() 119 | if err != nil { 120 | errchan <- err 121 | } 122 | }() 123 | timeoutCh := time.After(timeout) 124 | tick := time.Tick(250 * time.Millisecond) 125 | for { 126 | select { 127 | case <-timeoutCh: 128 | c.Stop() 129 | return errors.New("timeout whilst waiting for CA to start") 130 | case err := <-errchan: 131 | c.Stop() 132 | return errors.WithMessage(err, "failed to start CA") 133 | case <-tick: 134 | if c.hasStarted() { 135 | return nil 136 | } 137 | } 138 | } 139 | } 140 | 141 | // Stop stops the peer. 142 | func (c *CA) Stop() error { 143 | if c.command != nil { 144 | err := c.command.Process.Kill() 145 | if err != nil { 146 | return errors.WithMessage(err, "failed to stop peer") 147 | } 148 | c.command = nil 149 | } 150 | return nil 151 | } 152 | 153 | func (c *CA) hasStarted() bool { 154 | cli := &http.Client{ 155 | Transport: &http.Transport{ 156 | TLSClientConfig: &tls.Config{ 157 | InsecureSkipVerify: true, 158 | }, 159 | }, 160 | } 161 | resp, err := cli.Get(fmt.Sprintf("%s/healthz", c.OperationsURL(true))) 162 | if err != nil { 163 | log.Printf("error waiting for CA: %v\n", err) 164 | return false 165 | } 166 | if resp.StatusCode != 200 { 167 | return false 168 | } 169 | return true 170 | } 171 | -------------------------------------------------------------------------------- /internal/pkg/orderer/orderer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package orderer 6 | 7 | import ( 8 | "fmt" 9 | "net/url" 10 | "os/exec" 11 | 12 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 14 | ) 15 | 16 | // Orderer represents a loaded orderer definition. 17 | type Orderer struct { 18 | organization *organization.Organization 19 | mspID string 20 | identity *identity.Identity 21 | directory string 22 | microfabPort int32 23 | apiPort int32 24 | apiURL *url.URL 25 | operationsPort int32 26 | operationsURL *url.URL 27 | command *exec.Cmd 28 | tls *identity.Identity 29 | } 30 | 31 | // New creates a new orderer. 32 | func New(organization *organization.Organization, directory string, microFabPort int32, apiPort int32, apiURL string, operationsPort int32, operationsURL string) (*Orderer, error) { 33 | identityName := fmt.Sprintf("%s Orderer", organization.Name()) 34 | identity, err := identity.New(identityName, identity.WithOrganizationalUnit("orderer"), identity.UsingSigner(organization.CA())) 35 | if err != nil { 36 | return nil, err 37 | } 38 | parsedAPIURL, err := url.Parse(apiURL) 39 | if err != nil { 40 | return nil, err 41 | } 42 | parsedOperationsURL, err := url.Parse(operationsURL) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &Orderer{organization, organization.MSPID(), identity, directory, microFabPort, apiPort, parsedAPIURL, operationsPort, parsedOperationsURL, nil, nil}, nil 47 | } 48 | 49 | // TLS gets the TLS identity for this orderer. 50 | func (o *Orderer) TLS() *identity.Identity { 51 | return o.tls 52 | } 53 | 54 | // EnableTLS enables TLS for this orderer. 55 | func (o *Orderer) EnableTLS(tls *identity.Identity) { 56 | o.tls = tls 57 | } 58 | 59 | // Organization returns the organization of the orderer. 60 | func (o *Orderer) Organization() *organization.Organization { 61 | return o.organization 62 | } 63 | 64 | // MSPID returns the MSP ID of the orderer. 65 | func (o *Orderer) MSPID() string { 66 | return o.mspID 67 | } 68 | 69 | // APIHostname returns the hostname of the orderer. 70 | func (o *Orderer) APIHostname(internal bool) string { 71 | if internal { 72 | return "localhost" 73 | } 74 | return o.apiURL.Hostname() 75 | } 76 | 77 | // APIHost returns the host (hostname:port) of the orderer. 78 | func (o *Orderer) APIHost(internal bool) string { 79 | if internal { 80 | return fmt.Sprintf("localhost:%d", o.apiPort) 81 | } 82 | return fmt.Sprintf("%s:%d", o.APIHostname(false), o.microfabPort) 83 | } 84 | 85 | // APIPort returns the API port of the orderer. 86 | func (o *Orderer) APIPort(internal bool) int32 { 87 | if internal { 88 | return o.apiPort 89 | } 90 | return o.microfabPort 91 | } 92 | 93 | // APIURL returns the API URL of the orderer. 94 | func (o *Orderer) APIURL(internal bool) *url.URL { 95 | scheme := "grpc" 96 | if o.tls != nil { 97 | scheme = "grpcs" 98 | } 99 | if internal { 100 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, o.apiPort)) 101 | return url 102 | } 103 | 104 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, o.APIHost(false))) 105 | return url 106 | } 107 | 108 | // OperationsHostname returns the hostname of the orderer. 109 | func (o *Orderer) OperationsHostname(internal bool) string { 110 | if internal { 111 | return "localhost" 112 | } 113 | return o.operationsURL.Hostname() 114 | } 115 | 116 | // OperationsHost returns the host (hostname:port) of the orderer. 117 | func (o *Orderer) OperationsHost(internal bool) string { 118 | if internal { 119 | return fmt.Sprintf("localhost:%d", o.operationsPort) 120 | } 121 | return fmt.Sprintf("%s:%d", o.OperationsHostname(false), o.microfabPort) 122 | } 123 | 124 | // OperationsPort returns the operations port of the orderer. 125 | func (o *Orderer) OperationsPort(internal bool) int32 { 126 | if internal { 127 | return o.operationsPort 128 | } 129 | return o.microfabPort 130 | } 131 | 132 | // OperationsURL returns the operations URL of the orderer. 133 | func (o *Orderer) OperationsURL(internal bool) *url.URL { 134 | scheme := "http" 135 | if o.tls != nil { 136 | scheme = "https" 137 | } 138 | if internal { 139 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, o.operationsPort)) 140 | return url 141 | } 142 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, o.OperationsHost(false))) 143 | return url 144 | } 145 | -------------------------------------------------------------------------------- /scripts/test-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright IBM Corp. All Rights Reserved. 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | set -xeuo pipefail 8 | 9 | # Grab the current directory and make the cfg directory 10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )" 11 | export CFG=$DIR/_cfg 12 | rm -rf "${CFG}" || mkdir -p "${CFG}" 13 | mkdir -p "${CFG}/data" 14 | 15 | : ${MICROFAB_IMAGE:="ghcr.io/hyperledger-labs/microfab:latest"} 16 | 17 | if docker inspect microfab &>/dev/null; then 18 | echo "Removing existing microfab container:" 19 | docker kill microfab 20 | fi 21 | 22 | export MICROFAB_CONFIG='{ 23 | "couchdb":false, 24 | "endorsing_organizations":[ 25 | { 26 | "name": "org1" 27 | }, 28 | { 29 | "name":"org2" 30 | } 31 | 32 | ], 33 | "channels":[ 34 | { 35 | "name": "ch-a", 36 | "endorsing_organizations":[ 37 | "org1","org2" 38 | ] 39 | } 40 | ], 41 | "tls": { 42 | "enabled":true 43 | }, 44 | "capability_level":"V2_5" 45 | }' 46 | 47 | # docker run --name microfab -u $(id -u) -p 8080:8080 --add-host host.docker.internal:host-gateway \ 48 | # --rm -e MICROFAB_CONFIG="${MICROFAB_CONFIG}" \ 49 | # -e FABRIC_LOGGING_SPEC=info \ 50 | # -v "${CFG}/data":/home/microfab/data \ 51 | # ${MICROFAB_IMAGE} 52 | docker run -d --name microfab -p 8080:8080 --add-host host.docker.internal:host-gateway \ 53 | --rm -e MICROFAB_CONFIG="${MICROFAB_CONFIG}" \ 54 | -e FABRIC_LOGGING_SPEC=info \ 55 | ${MICROFAB_IMAGE} 56 | 57 | # Get the configuration and extract the information 58 | sleep 25 59 | 60 | curl -sSL --insecure https://console.127-0-0-1.nip.io:8080/ak/api/v1/components 61 | curl -sSL --insecure https://console.127-0-0-1.nip.io:8080/ak/api/v1/components | npx @hyperledger-labs/weft microfab -w $CFG/_wallets -p $CFG/_gateways -m $CFG/_msp -f 62 | 63 | # Chaincodes are all ready packaged up in the integration directory 64 | 65 | # set for peer 1 66 | export CORE_PEER_TLS_ENABLED=true 67 | export CORE_PEER_LOCALMSPID=org1MSP 68 | export CORE_PEER_TLS_ROOTCERT_FILE="${CFG}/_msp/tls/org1peer/tlsca-org1peer-cert.pem" 69 | export CORE_PEER_MSPCONFIGPATH="${CFG}/_msp/org1/org1admin/msp" 70 | export CORE_PEER_ADDRESS=org1peer-api.127-0-0-1.nip.io:8080 71 | export ORDERER_CA="$CFG/_msp/tls/orderer/tlsca-orderer-cert.pem" 72 | 73 | peer lifecycle chaincode install ${DIR}/integration/data/asset-transfer-basic-go.tgz 74 | export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') 75 | echo $PACKAGE_ID 76 | peer lifecycle chaincode approveformyorg --orderer orderer-api.127-0-0-1.nip.io:8080 \ 77 | --channelID ch-a \ 78 | --name basic-go \ 79 | -v 0 \ 80 | --package-id $PACKAGE_ID \ 81 | --sequence 1 \ 82 | --tls \ 83 | --cafile $ORDERER_CA 84 | 85 | peer lifecycle chaincode checkcommitreadiness --channelID ch-a --name basic-go -v 0 --sequence 1 86 | 87 | 88 | # set for peer 2 89 | 90 | export CORE_PEER_TLS_ENABLED=true 91 | export CORE_PEER_LOCALMSPID=org2MSP 92 | export CORE_PEER_TLS_ROOTCERT_FILE="${CFG}/_msp/tls/org2peer/tlsca-org2peer-cert.pem" 93 | export CORE_PEER_MSPCONFIGPATH="${CFG}/_msp/org2/org2admin/msp" 94 | export CORE_PEER_ADDRESS=org2peer-api.127-0-0-1.nip.io:8080 95 | export ORDERER_CA="$CFG/_msp/tls/orderer/tlsca-orderer-cert.pem" 96 | 97 | peer lifecycle chaincode install ${DIR}/integration/data/asset-transfer-basic-go.tgz 98 | export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') 99 | echo $PACKAGE_ID 100 | peer lifecycle chaincode approveformyorg --orderer orderer-api.127-0-0-1.nip.io:8080 \ 101 | --channelID ch-a \ 102 | --name basic-go \ 103 | -v 0 \ 104 | --package-id $PACKAGE_ID \ 105 | --sequence 1 \ 106 | --tls \ 107 | --cafile $ORDERER_CA 108 | 109 | peer lifecycle chaincode checkcommitreadiness --channelID ch-a --name basic-go -v 0 --sequence 1 110 | 111 | # commit as either org 112 | peer lifecycle chaincode commit --orderer orderer-api.127-0-0-1.nip.io:8080 \ 113 | --peerAddresses org1peer-api.127-0-0-1.nip.io:8080 --tlsRootCertFiles "${CFG}/_msp/tls/org1peer/tlsca-org1peer-cert.pem" \ 114 | --peerAddresses org2peer-api.127-0-0-1.nip.io:8080 --tlsRootCertFiles "${CFG}/_msp/tls/org2peer/tlsca-org2peer-cert.pem" \ 115 | --channelID ch-a --name basic-go -v 0 \ 116 | --sequence 1 \ 117 | --tls --cafile $ORDERER_CA --waitForEvent 118 | 119 | peer chaincode query -C ch-a -n basic-go -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' -------------------------------------------------------------------------------- /Dockerfile2: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | 5 | 6 | FROM golang:1.18 AS builder 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | # Build tools 10 | RUN apt-get update \ 11 | && apt-get -y install build-essential gcc gzip \ 12 | && apt-get -y install python3 python3-distutils libpython3-dev software-properties-common \ 13 | && apt-get -y install curl git jq unzip moreutils 14 | 15 | WORKDIR $GOPATH/src/github.com/hyperledger 16 | RUN git clone --depth 1 --branch release-2.5 https://github.com/hyperledger/fabric.git \ 17 | && git clone --depth 1 --branch v1.5.5 https://github.com/hyperledger/fabric-ca.git 18 | WORKDIR $GOPATH/src/github.com/hyperledger/fabric 19 | ENV CGO_ENABLED=0 20 | RUN make orderer \ 21 | && make tools \ 22 | && make ccaasbuilder 23 | WORKDIR $GOPATH/src/github.com/hyperledger/fabric-ca 24 | ENV CGO_ENABLED=1 25 | RUN make release/build/bin/fabric-ca-client \ 26 | && make release/build/bin/fabric-ca-server 27 | 28 | WORKDIR $GOPATH/src/github.com/IBM/microfab 29 | ENV CGO_ENABLED=0 30 | ADD . $GOPATH/src/github.com/IBM/microfab 31 | RUN go build -o microfabd cmd/microfabd/main.go 32 | 33 | WORKDIR /fabric 34 | RUN curl -sSL https://github.com/hyperledger/fabric/releases/download/v2.4.6/hyperledger-fabric-linux-amd64-2.4.6.tar.gz | tar xzf - config 35 | 36 | 37 | # RUN FABRIC_DOCKER_REGISTRY=ghcr.io/hyperledger \ 38 | # curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh \ 39 | # | bash -s -- binary --fabric-version 2.5.0-alpha1 --ca-version 1.5.6-beta 40 | 41 | FROM couchdb:3.1.2 42 | 43 | # Though they are populated by docker buildx, the ARG reference is required to make them available for use 44 | ARG TARGETARCH 45 | ARG TARGETOS 46 | 47 | RUN apt-get update && apt-get install -y \ 48 | jq xz-utils unzip vim\ 49 | && rm -rf /var/lib/apt/lists/* 50 | 51 | RUN groupadd -g 7051 microfab \ 52 | && useradd -u 7051 -g microfab -G root -s /bin/bash -m microfab \ 53 | && mkdir -p /home/microfab/builders /home/microfab/data \ 54 | && chown -R microfab:microfab /home/microfab 55 | 56 | # go1.19.4.linux-amd64.tar.gz 57 | # go1.19.4.linux-arm64.tar.gz 58 | RUN mkdir -p /opt/go /opt/node /opt/java \ 59 | && curl -sSL https://dl.google.com/go/go1.17.2.$TARGETOS-$TARGETARCH.tar.gz | tar xzf - -C /opt/go --strip-components=1 60 | 61 | # OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz 62 | # node-v18.12.1-linux-x64.tar.xz 63 | RUN if [ "${TARGETARCH}" = "amd64" ]; then ARCH=x64 \ 64 | && curl -sSL https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.17%2B8/OpenJDK11U-jdk_${ARCH}_${TARGETOS}_hotspot_11.0.17_8.tar.gz | tar xzf - -C /opt/java --strip-components=1 \ 65 | && curl -sSL https://nodejs.org/download/release/v16.4.0/node-v16.4.0-${TARGETOS}-${ARCH}.tar.xz | tar xJf - -C /opt/node --strip-components=1 \ 66 | ; fi 67 | 68 | # node-v18.12.1-linux-arm64.tar.xz 69 | # OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.17_8.tar.gz 70 | RUN if [ "${TARGETARCH}" = "arm64" ]; then ARCH=aarch64 \ 71 | && curl -sSL https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.17%2B8/OpenJDK11U-jdk_${ARCH}_${TARGETOS}_hotspot_11.0.17_8.tar.gz | tar xzf - -C /opt/java --strip-components=1 \ 72 | && curl -sSL https://nodejs.org/download/release/v16.4.0/node-v16.4.0-${TARGETOS}-${TARGETARCH}.tar.xz | tar xJf - -C /opt/node --strip-components=1 \ 73 | ; fi 74 | 75 | 76 | COPY docker/local.ini /opt/couchdb/etc/local.d/local.ini 77 | COPY docker/docker-entrypoint.sh /usr/local/bin 78 | COPY --from=builder /go/src/github.com/hyperledger/fabric/build/bin/* /usr/local/bin/ 79 | COPY --from=builder /go/src/github.com/hyperledger/fabric-ca/release/build/bin/* /usr/local/bin/ 80 | COPY --from=builder /fabric/config /etc/hyperledger/fabric 81 | COPY --from=builder /go/src/github.com/IBM/microfab/microfabd /usr/local/bin/ 82 | 83 | COPY --from=builder --chown=microfab:microfab /go/src/github.com/IBM/microfab/builders/ /home/microfab/builders/ 84 | COPY --from=builder --chown=microfab:microfab /go/src/github.com/hyperledger/fabric/release/*/builders/ccaas /home/microfab/builders/ccaas 85 | 86 | RUN sed -i 's/opt/home/g' /opt/couchdb/etc/local.d/local.ini 87 | 88 | ENV FABRIC_CFG_PATH=/etc/hyperledger/fabric 89 | ENV MICROFAB_HOME=/home/microfab 90 | ENV PATH=/opt/go/bin:/opt/node/bin:/opt/java/bin:/opt/couchdb/bin:${PATH} 91 | 92 | 93 | RUN curl -sSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-5.6.4-bin.zip \ 94 | && unzip -qq /tmp/gradle.zip -d /opt \ 95 | && mkdir -p /opt/gradle/bin \ 96 | && cd /opt/gradle/bin \ 97 | && /opt/gradle-5.6.4/bin/gradle wrapper \ 98 | && rm -f /tmp/gradle.zip \ 99 | && rm -rf /opt/gradle-5.6.4 \ 100 | && cd - \ 101 | && curl -sSL https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz | tar xzf - -C /opt \ 102 | && mv /opt/apache-maven-3.6.3 /opt/maven 103 | ENV PATH=/opt/gradle/bin:/opt/maven/bin:${PATH} 104 | ADD builders/java/pom.xml /opt/fabric-chaincode-java/ 105 | 106 | RUN cd /opt/fabric-chaincode-java \ 107 | && mvn -q dependency:copy-dependencies -DoutputDirectory=/opt/fabric-chaincode-java/lib \ 108 | && npm install --unsafe-perm -g fabric-shim@2.4.2 \ 109 | && rm -rf /tmp/gocache /tmp/goenv /tmp/go /tmp/maven /tmp/npm-cache /tmp/npm-devdir 110 | 111 | RUN chmod 666 /etc/passwd 112 | 113 | EXPOSE 8080 114 | USER 7051 115 | 116 | ENV GOROOT=/opt/go 117 | ENV GOCACHE=/tmp/gocache 118 | ENV GOENV=/tmp/goenv 119 | ENV GOPATH=/tmp/go 120 | 121 | VOLUME /home/microfab/data 122 | ENTRYPOINT [ "tini", "--", "/usr/local/bin/docker-entrypoint.sh" ] 123 | -------------------------------------------------------------------------------- /docs/Tutorial.md: -------------------------------------------------------------------------------- 1 | ## Tutorial: Running MIcrofab and deploying a Smart Contract 2 | 3 | _**Time**: 4 minutes_ 4 | 5 | _**Required setup**: Make sure you've docker installed as well as curl and a nodejs runtime (min v14.x)_ 6 | 7 | --- 8 | 9 | - Create a blank directory and change to it. Also a couple of terminal windows will be useful here 10 | - We're going to need the `peer` command from Fabric itself 11 | 12 | ``` 13 | curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary 14 | export PATH=$PATH:$(pwd)/bin 15 | export FABRIC_CFG_PATH=$(pwd)/config 16 | ``` 17 | 18 | - We need to get a SmartContract to test; let's get a pre-packaged one from the test suite in Microfab's own repo 19 | ``` 20 | curl -sSL https://github.com/hyperledger-labs/microfab/raw/main/integration/data/asset-transfer-basic-typescript.tgz -o asset-transfer-basic-typescript.tgz 21 | ``` 22 | 23 | - Start Microfab with it's default configuration; (in a separate terminal run `docker logs -f microfab` so you can see what it's doing) 24 | ``` 25 | curl -sSL https://github.com/hyperledger-labs/microfab/releases/download/v0.0.18/microfab-linux-amd64 -o microfab 26 | 27 | microfab start 28 | ``` 29 | 30 | - We need to get the configuration of microfab and the address identities that it created; using the Hyperledger Labs *weft* tool is the quickest 31 | 32 | ``` 33 | microfab connect 34 | ``` 35 | 36 | - This writes out a certificates and keys in a structure to use with the PeerCLI. Set the current shell enviroment variables for org1 37 | 38 | ``` 39 | source _mfcfg/org1.env 40 | ``` 41 | 42 | - We can then Install, Approve and Commit the chaincode definition 43 | 44 | ``` 45 | peer lifecycle chaincode install $(pwd)/asset-transfer-basic-typescript.tgz 46 | export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') 47 | 48 | peer lifecycle chaincode approveformyorg --orderer orderer-api.127-0-0-1.nip.io:8080 \ 49 | --channelID channel1 \ 50 | --name assettx \ 51 | -v 0 \ 52 | --package-id $PACKAGE_ID \ 53 | --sequence 1 54 | 55 | peer lifecycle chaincode commit --orderer orderer-api.127-0-0-1.nip.io:8080 \ 56 | --channelID channel1 \ 57 | --name assettx \ 58 | -v 0 \ 59 | --sequence 1 \ 60 | --waitForEvent 61 | ``` 62 | 63 | - Finally we can invoke a transaction, in this case a query on the Metadata of the contract. 64 | ``` 65 | peer chaincode query -C channel1 -n assettx -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' 66 | ``` 67 | 68 | - To submit a tranasction that needs to commit updates to the ledger, we need to use `peer chaincode invoke` 69 | ``` 70 | peer chaincode invoke -C channel1 -n assettx \ 71 | -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' \ 72 | --orderer orderer-api.127-0-0-1.nip.io:8080 73 | ``` 74 | 75 | Note for `invoke` the orderer needs to be specified. The output is also presented as escaped json. 76 | 77 | ``` 78 | peer chaincode invoke -C channel1 -n assettx \ 79 | -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' \ 80 | --orderer orderer-api.127-0-0-1.nip.io:8080 2>&1 \ 81 | git stat| sed -e 's/^.*payload://' | sed -e 's/..$//' -e 's/^.//' -e 's/\\"/"/g' | jq 82 | ``` 83 | 84 | ## Microfab CLI 85 | 86 | The CLI is a small binary wrapper that will create the docker image (pulling the image if needed), and write out the identitiy information. 87 | 88 | The (original) way was to run the docker commands manually, see below for the equivalents 89 | 90 | ``` 91 | Microfab Launch Control 92 | 93 | Usage: 94 | microfab [command] 95 | 96 | microfab 97 | connect Writes out connection details for use by the Peer CLI and SDKs 98 | ping Pings the microfab image to see if it's running 99 | start Starts the microfab image running 100 | stop Stops the microfab image running 101 | 102 | Additional Commands: 103 | completion Generate the autocompletion script for the specified shell 104 | help Help about any command 105 | 106 | Flags: 107 | -h, --help help for microfab 108 | -v, --version version for microfab 109 | 110 | ``` 111 | 112 | ### Start 113 | ``` 114 | Starts the microfab image running 115 | 116 | Usage: 117 | microfab start [flags] 118 | 119 | Flags: 120 | --config string Microfab config (default "{\"endorsing_organizations\":[{\"name\":\"org1\"}],\"channels\":[{\"name\":\"mychannel\",\"endorsing_organizations\":[\"org1\"]},{\"name\":\"appchannel\",\"endorsing_organizations\":[\"org1\"]}],\"capability_level\":\"V2_5\"}") 121 | --configFile string Microfab config file 122 | -f, --force Force restart if microfab already running 123 | -h, --help help for start 124 | -l, --logs Display the logs (docker logs -f microfab) 125 | ``` 126 | 127 | ### Connect 128 | 129 | ``` 130 | Writes out connection details for use by the Peer CLI and SDKs 131 | 132 | Usage: 133 | microfab connect [flags] 134 | 135 | Flags: 136 | -f, --force Force overwriting details directory 137 | -h, --help help for connect 138 | --msp string msp output directory (default "_mfcfg") 139 | ``` 140 | 141 | ## Docker Command Equivalents 142 | 143 | ``` 144 | docker run -d --rm -p 8080:8080 --name microfab ghcr.io/hyperledger-labs/microfab:latest 145 | ``` 146 | 147 | ``` 148 | curl -s http://console.127-0-0-1.nip.io:8080/ak/api/v1/components | npx @hyperledger-labs/weft microfab -w _wallets -p _gateways -m _msp -f 149 | ``` -------------------------------------------------------------------------------- /internal/pkg/peer/peer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "fmt" 9 | "net/url" 10 | "os/exec" 11 | 12 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 14 | ) 15 | 16 | // Peer represents a loaded peer definition. 17 | type Peer struct { 18 | organization *organization.Organization 19 | identity *identity.Identity 20 | mspID string 21 | directory string 22 | microfabPort int32 23 | apiPort int32 24 | apiURL *url.URL 25 | chaincodePort int32 26 | chaincodeURL *url.URL 27 | operationsPort int32 28 | operationsURL *url.URL 29 | couchDB bool 30 | couchDBPort int32 31 | gossipPort int32 32 | gossipURL *url.URL 33 | command *exec.Cmd 34 | tls *identity.Identity 35 | } 36 | 37 | // New creates a new peer. 38 | func New(organization *organization.Organization, directory string, microfabPort int32, apiPort int32, apiURL string, chaincodePort int32, chaincodeURL string, operationsPort int32, operationsURL string, couchDB bool, couchDBPort int32, gossipPort int32, gossipURL string) (*Peer, error) { 39 | identityName := fmt.Sprintf("%s Peer", organization.Name()) 40 | identity, err := identity.New(identityName, identity.WithOrganizationalUnit("peer"), identity.UsingSigner(organization.CA())) 41 | if err != nil { 42 | return nil, err 43 | } 44 | parsedAPIURL, err := url.Parse(apiURL) 45 | if err != nil { 46 | return nil, err 47 | } 48 | parsedChaincodeURL, err := url.Parse(chaincodeURL) 49 | if err != nil { 50 | return nil, err 51 | } 52 | parsedOperationsURL, err := url.Parse(operationsURL) 53 | if err != nil { 54 | return nil, err 55 | } 56 | parsedGossipURL, err := url.Parse(gossipURL) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return &Peer{organization, identity, organization.MSPID(), directory, microfabPort, apiPort, parsedAPIURL, chaincodePort, parsedChaincodeURL, operationsPort, parsedOperationsURL, couchDB, couchDBPort, gossipPort, parsedGossipURL, nil, nil}, nil 62 | } 63 | 64 | // TLS gets the TLS identity for this peer. 65 | func (p *Peer) TLS() *identity.Identity { 66 | return p.tls 67 | } 68 | 69 | // EnableTLS enables TLS for this peer. 70 | func (p *Peer) EnableTLS(tls *identity.Identity) { 71 | p.tls = tls 72 | } 73 | 74 | // Organization returns the organization of the peer. 75 | func (p *Peer) Organization() *organization.Organization { 76 | return p.organization 77 | } 78 | 79 | // MSPID returns the MSP ID of the peer. 80 | func (p *Peer) MSPID() string { 81 | return p.mspID 82 | } 83 | 84 | // APIHostname returns the hostname of the peer. 85 | func (p *Peer) APIHostname(internal bool) string { 86 | if internal { 87 | return "localhost" 88 | } 89 | return p.apiURL.Hostname() 90 | } 91 | 92 | // APIHost returns the host (hostname:port) of the peer. 93 | func (p *Peer) APIHost(internal bool) string { 94 | if internal { 95 | return fmt.Sprintf("localhost:%d", p.apiPort) 96 | } 97 | return fmt.Sprintf("%s:%d", p.APIHostname(false), p.microfabPort) 98 | } 99 | 100 | // APIPort returns the API port of the peer. 101 | func (p *Peer) APIPort(internal bool) int32 { 102 | if internal { 103 | return p.apiPort 104 | } 105 | 106 | return p.microfabPort 107 | } 108 | 109 | // APIURL returns the API URL of the peer. 110 | func (p *Peer) APIURL(internal bool) *url.URL { 111 | scheme := "grpc" 112 | if p.tls != nil { 113 | scheme = "grpcs" 114 | } 115 | if internal { 116 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, p.apiPort)) 117 | return url 118 | } 119 | 120 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, p.APIHost(false))) 121 | return url 122 | } 123 | 124 | // ChaincodeHostname returns the hostname of the peer. 125 | func (p *Peer) ChaincodeHostname(internal bool) string { 126 | if internal { 127 | return "localhost" 128 | } 129 | return p.chaincodeURL.Hostname() 130 | } 131 | 132 | // ChaincodeHost returns the host (hostname:port) of the peer. 133 | func (p *Peer) ChaincodeHost(internal bool) string { 134 | if internal { 135 | return fmt.Sprintf("localhost:%d", p.chaincodePort) 136 | } 137 | return fmt.Sprintf("%s:%d", p.ChaincodeHostname(false), p.microfabPort) 138 | } 139 | 140 | // ChaincodePort returns the chaincode port of the peer. 141 | func (p *Peer) ChaincodePort(internal bool) int32 { 142 | if internal { 143 | return p.chaincodePort 144 | } 145 | return p.microfabPort 146 | } 147 | 148 | // ChaincodeURL returns the chaincode URL of the peer. 149 | func (p *Peer) ChaincodeURL(internal bool) *url.URL { 150 | scheme := "grpc" 151 | if p.tls != nil { 152 | scheme = "grpcs" 153 | } 154 | if internal { 155 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, p.chaincodePort)) 156 | return url 157 | } 158 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, p.ChaincodeHost(false))) 159 | return url 160 | } 161 | 162 | // OperationsHostname returns the hostname of the peer. 163 | func (p *Peer) OperationsHostname(internal bool) string { 164 | if internal { 165 | return "localhost" 166 | } 167 | return p.operationsURL.Hostname() 168 | } 169 | 170 | // OperationsHost returns the host (hostname:port) of the peer. 171 | func (p *Peer) OperationsHost(internal bool) string { 172 | if internal { 173 | return fmt.Sprintf("localhost:%d", p.operationsPort) 174 | } 175 | return fmt.Sprintf("%s:%d", p.OperationsHostname(false), p.microfabPort) 176 | } 177 | 178 | // OperationsPort returns the operations port of the peer. 179 | func (p *Peer) OperationsPort(internal bool) int32 { 180 | if internal { 181 | return p.operationsPort 182 | } 183 | return p.microfabPort 184 | } 185 | 186 | // OperationsURL returns the operations URL of the peer. 187 | func (p *Peer) OperationsURL(internal bool) *url.URL { 188 | scheme := "http" 189 | if p.tls != nil { 190 | scheme = "https" 191 | } 192 | if internal { 193 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, p.operationsPort)) 194 | return url 195 | } 196 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, p.OperationsHost(false))) 197 | return url 198 | } 199 | 200 | // GossipHost returns the host of the gossip connection 201 | func (p *Peer) GossipHost(internal bool) string { 202 | if internal { 203 | return fmt.Sprintf("localhost:%d", p.gossipPort) 204 | } 205 | return fmt.Sprintf("%s:%d", p.GossipHostname(false), p.microfabPort) 206 | } 207 | 208 | // GossipHostname returns just the host name used for the gossip connection 209 | func (p *Peer) GossipHostname(internal bool) string { 210 | if internal { 211 | return "localhost" 212 | } 213 | return p.gossipURL.Hostname() 214 | } 215 | 216 | // GossipURL returns the full URL used for the gossip connection 217 | func (p *Peer) GossipURL(internal bool) *url.URL { 218 | scheme := "http" 219 | if p.tls != nil { 220 | scheme = "https" 221 | } 222 | if internal { 223 | url, _ := url.Parse(fmt.Sprintf("%s://localhost:%d", scheme, p.gossipPort)) 224 | return url 225 | } 226 | url, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, p.GossipHost(false))) 227 | return url 228 | } 229 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package client 6 | 7 | import ( 8 | "crypto/tls" 9 | "encoding/json" 10 | "net/http" 11 | "net/url" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Client represents a Microfab client. 17 | type Client struct { 18 | url *url.URL 19 | httpClient *http.Client 20 | tlsEnabled bool 21 | } 22 | 23 | // Options represents connection options for a peer or ordering service. 24 | type Options struct { 25 | DefaultAuthority string `json:"grpc.default_authority"` 26 | SSLTargetNameOverride string `json:"grpc.ssl_target_name_override"` 27 | RequestTimeout int `json:"request-timeout"` 28 | } 29 | 30 | // Peer represents a peer running inside Microfab. 31 | type Peer struct { 32 | ID string `json:"id"` 33 | DisplayName string `json:"display_name"` 34 | Type string `json:"type"` 35 | APIURL string `json:"api_url"` 36 | APIOptions *Options `json:"api_options"` 37 | ChaincodeURL string `json:"chaincode_url"` 38 | ChaincodeOptions *Options `json:"chaincode_options"` 39 | OperationsURL string `json:"operations_url"` 40 | OperationsOptions *Options `json:"operations_options"` 41 | MSPID string `json:"msp_id"` 42 | Wallet string `json:"wallet"` 43 | Identity string `json:"identity"` 44 | } 45 | 46 | // OrderingService represents an ordering service running inside Microfab. 47 | type OrderingService struct { 48 | ID string `json:"id"` 49 | DisplayName string `json:"display_name"` 50 | Type string `json:"type"` 51 | APIURL string `json:"api_url"` 52 | APIOptions *Options `json:"api_options"` 53 | OperationsURL string `json:"operations_url"` 54 | OperationsOptions *Options `json:"operations_options"` 55 | MSPID string `json:"msp_id"` 56 | Wallet string `json:"wallet"` 57 | Identity string `json:"identity"` 58 | } 59 | 60 | // Identity represents an identity used for managing components inside Microfab. 61 | type Identity struct { 62 | ID string `json:"id"` 63 | DisplayName string `json:"display_name"` 64 | Type string `json:"type"` 65 | Certificate []byte `json:"cert"` 66 | PrivateKey []byte `json:"private_key"` 67 | CA []byte `json:"ca"` 68 | MSPID string `json:"msp_id"` 69 | Wallet string `json:"wallet"` 70 | } 71 | 72 | // New creates a new Microfab client. 73 | func New(url *url.URL, tlsEnabled bool) (*Client, error) { 74 | 75 | if tlsEnabled { 76 | httpClient := &http.Client{ 77 | Transport: &http.Transport{ 78 | TLSClientConfig: &tls.Config{ 79 | InsecureSkipVerify: true, 80 | }, 81 | }, 82 | } 83 | return &Client{ 84 | url: url, 85 | tlsEnabled: true, 86 | httpClient: httpClient, 87 | }, nil 88 | } 89 | 90 | return &Client{ 91 | url: url, 92 | tlsEnabled: false, 93 | httpClient: http.DefaultClient, 94 | }, nil 95 | 96 | } 97 | 98 | // Ping tests the connection to Microfab. 99 | func (c *Client) Ping() error { 100 | 101 | target := c.url.ResolveReference(&url.URL{Path: "/ak/api/v1/health"}) 102 | resp, err := c.httpClient.Get(target.String()) 103 | if err != nil { 104 | return err 105 | } else if resp.StatusCode < 200 || resp.StatusCode >= 300 { 106 | return errors.Errorf("Microfab returned HTTP %s", resp.Status) 107 | } 108 | return nil 109 | } 110 | 111 | // GetOrganizations gets the names of all of the organizations. 112 | func (c *Client) GetOrganizations() ([]string, error) { 113 | components, err := c.getComponents() 114 | if err != nil { 115 | return nil, err 116 | } 117 | organizationNames := map[string]bool{} 118 | for _, component := range components { 119 | wallet, ok := component["wallet"] 120 | if !ok { 121 | continue 122 | } 123 | organizationNames[wallet.(string)] = true 124 | } 125 | result := []string{} 126 | for organizationName := range organizationNames { 127 | result = append(result, organizationName) 128 | } 129 | return result, nil 130 | } 131 | 132 | // GetPeer gets the peer for the specified organization. 133 | func (c *Client) GetPeer(organization string) (*Peer, error) { 134 | components, err := c.getComponents() 135 | if err != nil { 136 | return nil, err 137 | } 138 | for _, component := range components { 139 | ctype, ok := component["type"] 140 | if !ok { 141 | continue 142 | } 143 | wallet, ok := component["wallet"] 144 | if !ok { 145 | continue 146 | } else if ctype == "fabric-peer" && wallet == organization { 147 | data, err := json.Marshal(component) 148 | if err != nil { 149 | return nil, err 150 | } 151 | result := &Peer{} 152 | err = json.Unmarshal(data, result) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return result, nil 157 | } 158 | } 159 | return nil, errors.Errorf("Microfab does not have a peer for organization %s", organization) 160 | } 161 | 162 | // GetOrderingService gets the ordering service. 163 | func (c *Client) GetOrderingService() (*OrderingService, error) { 164 | components, err := c.getComponents() 165 | if err != nil { 166 | return nil, err 167 | } 168 | for _, component := range components { 169 | ctype, ok := component["type"] 170 | if !ok { 171 | continue 172 | } else if ctype == "fabric-orderer" { 173 | data, err := json.Marshal(component) 174 | if err != nil { 175 | return nil, err 176 | } 177 | result := &OrderingService{} 178 | err = json.Unmarshal(data, result) 179 | if err != nil { 180 | return nil, err 181 | } 182 | return result, nil 183 | } 184 | } 185 | return nil, errors.New("Microfab does not have an ordering service") 186 | } 187 | 188 | // GetIdentity gets the identity for the specified organization. 189 | func (c *Client) GetIdentity(organization string) (*Identity, error) { 190 | components, err := c.getComponents() 191 | if err != nil { 192 | return nil, err 193 | } 194 | for _, component := range components { 195 | ctype, ok := component["type"] 196 | if !ok { 197 | continue 198 | } 199 | wallet, ok := component["wallet"] 200 | if !ok { 201 | continue 202 | } else if ctype == "identity" && wallet == organization { 203 | data, err := json.Marshal(component) 204 | if err != nil { 205 | return nil, err 206 | } 207 | result := &Identity{} 208 | err = json.Unmarshal(data, result) 209 | if err != nil { 210 | return nil, err 211 | } 212 | return result, nil 213 | } 214 | } 215 | return nil, errors.Errorf("Microfab does not have an admin identity for organization %s", organization) 216 | } 217 | 218 | func (c *Client) getComponents() ([]map[string]interface{}, error) { 219 | target := c.url.ResolveReference(&url.URL{Path: "/ak/api/v1/components"}) 220 | resp, err := c.httpClient.Get(target.String()) 221 | if err != nil { 222 | return nil, err 223 | } else if resp.StatusCode < 200 || resp.StatusCode >= 300 { 224 | return nil, errors.Errorf("Microfab returned HTTP %s", resp.Status) 225 | } 226 | components := []map[string]interface{}{} 227 | err = json.NewDecoder(resp.Body).Decode(&components) 228 | if err != nil { 229 | return nil, err 230 | } 231 | return components, nil 232 | } 233 | -------------------------------------------------------------------------------- /internal/pkg/blocks/fakes/deliverer.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/hyperledger-labs/microfab/internal/pkg/blocks" 8 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 9 | "github.com/hyperledger/fabric-protos-go/common" 10 | ) 11 | 12 | type Deliverer struct { 13 | DeliverStub func(*common.Envelope, blocks.DeliverCallback) error 14 | deliverMutex sync.RWMutex 15 | deliverArgsForCall []struct { 16 | arg1 *common.Envelope 17 | arg2 blocks.DeliverCallback 18 | } 19 | deliverReturns struct { 20 | result1 error 21 | } 22 | deliverReturnsOnCall map[int]struct { 23 | result1 error 24 | } 25 | IdentityStub func() *identity.Identity 26 | identityMutex sync.RWMutex 27 | identityArgsForCall []struct { 28 | } 29 | identityReturns struct { 30 | result1 *identity.Identity 31 | } 32 | identityReturnsOnCall map[int]struct { 33 | result1 *identity.Identity 34 | } 35 | MSPIDStub func() string 36 | mSPIDMutex sync.RWMutex 37 | mSPIDArgsForCall []struct { 38 | } 39 | mSPIDReturns struct { 40 | result1 string 41 | } 42 | mSPIDReturnsOnCall map[int]struct { 43 | result1 string 44 | } 45 | invocations map[string][][]interface{} 46 | invocationsMutex sync.RWMutex 47 | } 48 | 49 | func (fake *Deliverer) Deliver(arg1 *common.Envelope, arg2 blocks.DeliverCallback) error { 50 | fake.deliverMutex.Lock() 51 | ret, specificReturn := fake.deliverReturnsOnCall[len(fake.deliverArgsForCall)] 52 | fake.deliverArgsForCall = append(fake.deliverArgsForCall, struct { 53 | arg1 *common.Envelope 54 | arg2 blocks.DeliverCallback 55 | }{arg1, arg2}) 56 | fake.recordInvocation("Deliver", []interface{}{arg1, arg2}) 57 | fake.deliverMutex.Unlock() 58 | if fake.DeliverStub != nil { 59 | return fake.DeliverStub(arg1, arg2) 60 | } 61 | if specificReturn { 62 | return ret.result1 63 | } 64 | fakeReturns := fake.deliverReturns 65 | return fakeReturns.result1 66 | } 67 | 68 | func (fake *Deliverer) DeliverCallCount() int { 69 | fake.deliverMutex.RLock() 70 | defer fake.deliverMutex.RUnlock() 71 | return len(fake.deliverArgsForCall) 72 | } 73 | 74 | func (fake *Deliverer) DeliverCalls(stub func(*common.Envelope, blocks.DeliverCallback) error) { 75 | fake.deliverMutex.Lock() 76 | defer fake.deliverMutex.Unlock() 77 | fake.DeliverStub = stub 78 | } 79 | 80 | func (fake *Deliverer) DeliverArgsForCall(i int) (*common.Envelope, blocks.DeliverCallback) { 81 | fake.deliverMutex.RLock() 82 | defer fake.deliverMutex.RUnlock() 83 | argsForCall := fake.deliverArgsForCall[i] 84 | return argsForCall.arg1, argsForCall.arg2 85 | } 86 | 87 | func (fake *Deliverer) DeliverReturns(result1 error) { 88 | fake.deliverMutex.Lock() 89 | defer fake.deliverMutex.Unlock() 90 | fake.DeliverStub = nil 91 | fake.deliverReturns = struct { 92 | result1 error 93 | }{result1} 94 | } 95 | 96 | func (fake *Deliverer) DeliverReturnsOnCall(i int, result1 error) { 97 | fake.deliverMutex.Lock() 98 | defer fake.deliverMutex.Unlock() 99 | fake.DeliverStub = nil 100 | if fake.deliverReturnsOnCall == nil { 101 | fake.deliverReturnsOnCall = make(map[int]struct { 102 | result1 error 103 | }) 104 | } 105 | fake.deliverReturnsOnCall[i] = struct { 106 | result1 error 107 | }{result1} 108 | } 109 | 110 | func (fake *Deliverer) Identity() *identity.Identity { 111 | fake.identityMutex.Lock() 112 | ret, specificReturn := fake.identityReturnsOnCall[len(fake.identityArgsForCall)] 113 | fake.identityArgsForCall = append(fake.identityArgsForCall, struct { 114 | }{}) 115 | fake.recordInvocation("Identity", []interface{}{}) 116 | fake.identityMutex.Unlock() 117 | if fake.IdentityStub != nil { 118 | return fake.IdentityStub() 119 | } 120 | if specificReturn { 121 | return ret.result1 122 | } 123 | fakeReturns := fake.identityReturns 124 | return fakeReturns.result1 125 | } 126 | 127 | func (fake *Deliverer) IdentityCallCount() int { 128 | fake.identityMutex.RLock() 129 | defer fake.identityMutex.RUnlock() 130 | return len(fake.identityArgsForCall) 131 | } 132 | 133 | func (fake *Deliverer) IdentityCalls(stub func() *identity.Identity) { 134 | fake.identityMutex.Lock() 135 | defer fake.identityMutex.Unlock() 136 | fake.IdentityStub = stub 137 | } 138 | 139 | func (fake *Deliverer) IdentityReturns(result1 *identity.Identity) { 140 | fake.identityMutex.Lock() 141 | defer fake.identityMutex.Unlock() 142 | fake.IdentityStub = nil 143 | fake.identityReturns = struct { 144 | result1 *identity.Identity 145 | }{result1} 146 | } 147 | 148 | func (fake *Deliverer) IdentityReturnsOnCall(i int, result1 *identity.Identity) { 149 | fake.identityMutex.Lock() 150 | defer fake.identityMutex.Unlock() 151 | fake.IdentityStub = nil 152 | if fake.identityReturnsOnCall == nil { 153 | fake.identityReturnsOnCall = make(map[int]struct { 154 | result1 *identity.Identity 155 | }) 156 | } 157 | fake.identityReturnsOnCall[i] = struct { 158 | result1 *identity.Identity 159 | }{result1} 160 | } 161 | 162 | func (fake *Deliverer) MSPID() string { 163 | fake.mSPIDMutex.Lock() 164 | ret, specificReturn := fake.mSPIDReturnsOnCall[len(fake.mSPIDArgsForCall)] 165 | fake.mSPIDArgsForCall = append(fake.mSPIDArgsForCall, struct { 166 | }{}) 167 | fake.recordInvocation("MSPID", []interface{}{}) 168 | fake.mSPIDMutex.Unlock() 169 | if fake.MSPIDStub != nil { 170 | return fake.MSPIDStub() 171 | } 172 | if specificReturn { 173 | return ret.result1 174 | } 175 | fakeReturns := fake.mSPIDReturns 176 | return fakeReturns.result1 177 | } 178 | 179 | func (fake *Deliverer) MSPIDCallCount() int { 180 | fake.mSPIDMutex.RLock() 181 | defer fake.mSPIDMutex.RUnlock() 182 | return len(fake.mSPIDArgsForCall) 183 | } 184 | 185 | func (fake *Deliverer) MSPIDCalls(stub func() string) { 186 | fake.mSPIDMutex.Lock() 187 | defer fake.mSPIDMutex.Unlock() 188 | fake.MSPIDStub = stub 189 | } 190 | 191 | func (fake *Deliverer) MSPIDReturns(result1 string) { 192 | fake.mSPIDMutex.Lock() 193 | defer fake.mSPIDMutex.Unlock() 194 | fake.MSPIDStub = nil 195 | fake.mSPIDReturns = struct { 196 | result1 string 197 | }{result1} 198 | } 199 | 200 | func (fake *Deliverer) MSPIDReturnsOnCall(i int, result1 string) { 201 | fake.mSPIDMutex.Lock() 202 | defer fake.mSPIDMutex.Unlock() 203 | fake.MSPIDStub = nil 204 | if fake.mSPIDReturnsOnCall == nil { 205 | fake.mSPIDReturnsOnCall = make(map[int]struct { 206 | result1 string 207 | }) 208 | } 209 | fake.mSPIDReturnsOnCall[i] = struct { 210 | result1 string 211 | }{result1} 212 | } 213 | 214 | func (fake *Deliverer) Invocations() map[string][][]interface{} { 215 | fake.invocationsMutex.RLock() 216 | defer fake.invocationsMutex.RUnlock() 217 | fake.deliverMutex.RLock() 218 | defer fake.deliverMutex.RUnlock() 219 | fake.identityMutex.RLock() 220 | defer fake.identityMutex.RUnlock() 221 | fake.mSPIDMutex.RLock() 222 | defer fake.mSPIDMutex.RUnlock() 223 | copiedInvocations := map[string][][]interface{}{} 224 | for key, value := range fake.invocations { 225 | copiedInvocations[key] = value 226 | } 227 | return copiedInvocations 228 | } 229 | 230 | func (fake *Deliverer) recordInvocation(key string, args []interface{}) { 231 | fake.invocationsMutex.Lock() 232 | defer fake.invocationsMutex.Unlock() 233 | if fake.invocations == nil { 234 | fake.invocations = map[string][][]interface{}{} 235 | } 236 | if fake.invocations[key] == nil { 237 | fake.invocations[key] = [][]interface{}{} 238 | } 239 | fake.invocations[key] = append(fake.invocations[key], args) 240 | } 241 | 242 | var _ blocks.Deliverer = new(Deliverer) 243 | -------------------------------------------------------------------------------- /internal/pkg/identity/identity.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package identity 6 | 7 | import ( 8 | "crypto" 9 | "crypto/ecdsa" 10 | "crypto/elliptic" 11 | "crypto/rand" 12 | "crypto/sha256" 13 | "crypto/x509" 14 | "crypto/x509/pkix" 15 | "encoding/asn1" 16 | "encoding/pem" 17 | "fmt" 18 | "log" 19 | "math/big" 20 | "os" 21 | "strings" 22 | "time" 23 | 24 | "github.com/hyperledger-labs/microfab/internal/pkg/identity/certificate" 25 | "github.com/hyperledger-labs/microfab/internal/pkg/identity/privatekey" 26 | "github.com/hyperledger-labs/microfab/pkg/client" 27 | ) 28 | 29 | var logger = log.New(os.Stdout, fmt.Sprintf("[%16s] ", "console"), log.LstdFlags) 30 | 31 | // Identity represents a loaded identity (X509 certificate and ECDSA private key pair). 32 | type Identity struct { 33 | name string 34 | certificate *certificate.Certificate 35 | privateKey *privatekey.PrivateKey 36 | ca *certificate.Certificate 37 | isCA bool 38 | } 39 | 40 | type newIdentity struct { 41 | Template *x509.Certificate 42 | Parent *x509.Certificate 43 | Signee *ecdsa.PublicKey 44 | Signer *ecdsa.PrivateKey 45 | } 46 | 47 | // Option is a type representing an option for creating a new identity. 48 | type Option func(*newIdentity) 49 | 50 | // UsingSigner uses the specified identity to sign the new identity. 51 | func UsingSigner(signer *Identity) Option { 52 | return func(o *newIdentity) { 53 | o.Template.AuthorityKeyId = signer.certificate.Certificate().SubjectKeyId 54 | o.Parent = signer.certificate.Certificate() 55 | o.Signer = signer.privateKey.PrivateKey() 56 | } 57 | } 58 | 59 | // WithIsCA indicates whether or not the new identity is a CA. 60 | func WithIsCA(isCA bool) Option { 61 | return func(o *newIdentity) { 62 | if isCA { 63 | o.Template.KeyUsage |= x509.KeyUsageCertSign | x509.KeyUsageCRLSign 64 | o.Template.IsCA = true 65 | } 66 | } 67 | } 68 | 69 | // WithOrganizationalUnit sets the OU field in the new identity. 70 | func WithOrganizationalUnit(organizationalUnit string) Option { 71 | return func(o *newIdentity) { 72 | o.Template.Subject.OrganizationalUnit = []string{organizationalUnit} 73 | } 74 | } 75 | 76 | // New creates a new identity. 77 | func New(name string, opts ...Option) (*Identity, error) { 78 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 79 | if err != nil { 80 | return nil, err 81 | } 82 | notBefore := time.Now().Add(-5 * time.Minute).UTC() 83 | notAfter := notBefore.Add(10 * 365 * 24 * time.Hour) 84 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 85 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | logger.Printf("Creating new x509 cert '%s'", name) 91 | 92 | identity := &newIdentity{ 93 | Template: &x509.Certificate{ 94 | NotBefore: notBefore, 95 | NotAfter: notAfter, 96 | SerialNumber: serialNumber, 97 | BasicConstraintsValid: true, 98 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 99 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 100 | Subject: pkix.Name{ 101 | CommonName: name, 102 | }, 103 | IsCA: false, 104 | DNSNames: []string{"*.127-0-0-1.nip.io", "127.0.0.1", "localhost", "0.0.0.0", "*.localho.st"}, 105 | }, 106 | } 107 | identity.Parent = identity.Template 108 | identity.Signee = &privateKey.PublicKey 109 | identity.Signer = privateKey 110 | for _, opt := range opts { 111 | opt(identity) 112 | } 113 | publicKeyBytes := elliptic.Marshal(identity.Signee.Curve, identity.Signee.X, identity.Signee.Y) 114 | subjectKeyID := sha256.Sum256(publicKeyBytes) 115 | identity.Template.SubjectKeyId = subjectKeyID[:] 116 | bytes, err := x509.CreateCertificate(rand.Reader, identity.Template, identity.Parent, identity.Signee, identity.Signer) 117 | if err != nil { 118 | return nil, err 119 | } 120 | cert, err := certificate.FromBytes(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: bytes})) 121 | if err != nil { 122 | return nil, err 123 | } 124 | bytes, err = x509.MarshalPKCS8PrivateKey(privateKey) 125 | if err != nil { 126 | return nil, err 127 | } 128 | pk, err := privatekey.FromBytes(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: bytes})) 129 | if err != nil { 130 | return nil, err 131 | } 132 | var ca *certificate.Certificate 133 | if identity.Template != identity.Parent { 134 | ca, err = certificate.FromBytes(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: identity.Parent.Raw})) 135 | if err != nil { 136 | return nil, err 137 | } 138 | } 139 | isCA := identity.Template.IsCA 140 | return &Identity{name, cert, pk, ca, isCA}, nil 141 | } 142 | 143 | // FromClient loads an identity from a client identity object. 144 | func FromClient(c *client.Identity) (*Identity, error) { 145 | name := c.DisplayName 146 | cert, err := certificate.FromBytes(c.Certificate) 147 | if err != nil { 148 | return nil, err 149 | } 150 | pk, err := privatekey.FromBytes(c.PrivateKey) 151 | if err != nil { 152 | return nil, err 153 | } 154 | var ca *certificate.Certificate 155 | if c.CA != nil { 156 | ca, err = certificate.FromBytes(c.CA) 157 | if err != nil { 158 | return nil, err 159 | } 160 | } 161 | return &Identity{name, cert, pk, ca, ca == nil}, nil 162 | } 163 | 164 | // ToClient saves an identity into a client identity object. 165 | func (i *Identity) ToClient() *client.Identity { 166 | id := strings.ToLower(i.name) 167 | var ca []byte 168 | if !i.isCA { 169 | ca = i.CA().Bytes() 170 | } 171 | return &client.Identity{ 172 | ID: id, 173 | DisplayName: i.name, 174 | Type: "identity", 175 | Certificate: i.Certificate().Bytes(), 176 | PrivateKey: i.PrivateKey().Bytes(), 177 | CA: ca, 178 | } 179 | } 180 | 181 | // FromParts creates an identity from the specified parts. 182 | func FromParts(name string, cert *certificate.Certificate, pk *privatekey.PrivateKey, ca *certificate.Certificate) (*Identity, error) { 183 | return &Identity{name, cert, pk, ca, ca == nil}, nil 184 | } 185 | 186 | // Name returns the name of the identity. 187 | func (i *Identity) Name() string { 188 | return i.name 189 | } 190 | 191 | // Certificate returns the loaded X509 certificate. 192 | func (i *Identity) Certificate() *certificate.Certificate { 193 | return i.certificate 194 | } 195 | 196 | // PrivateKey returns the loaded ECDSA private key. 197 | func (i *Identity) PrivateKey() *privatekey.PrivateKey { 198 | return i.privateKey 199 | } 200 | 201 | // CA returns the loaded X509 CA. 202 | func (i *Identity) CA() *certificate.Certificate { 203 | return i.ca 204 | } 205 | 206 | // Sign returns a signature of the SHA256 hash over the specified data. 207 | func (i *Identity) Sign(data ...[]byte) []byte { 208 | hasher := sha256.New() 209 | for _, d := range data { 210 | hasher.Write(d) 211 | } 212 | hash := hasher.Sum(nil) 213 | signature, err := i.PrivateKey().PrivateKey().Sign(rand.Reader, hash, crypto.SHA256) 214 | if err != nil { 215 | panic(err) 216 | } 217 | return i.preventMallebility(signature) 218 | } 219 | 220 | func (i *Identity) preventMallebility(signature []byte) []byte { 221 | var parts struct { 222 | R, S *big.Int 223 | } 224 | _, err := asn1.Unmarshal(signature, &parts) 225 | if err != nil { 226 | panic(err) 227 | } 228 | halfOrder := new(big.Int).Rsh(elliptic.P256().Params().N, 1) 229 | if parts.S.Cmp(halfOrder) == 1 { 230 | parts.S.Sub(i.PrivateKey().PublicKey().Params().N, parts.S) 231 | signature, err = asn1.Marshal(parts) 232 | if err != nil { 233 | panic(err) 234 | } 235 | } 236 | return signature 237 | } 238 | -------------------------------------------------------------------------------- /internal/pkg/channel/chaincode.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package channel 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/hyperledger-labs/microfab/internal/pkg/blocks" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/orderer" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/peer" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/protoutil" 14 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 15 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 16 | "github.com/hyperledger/fabric-protos-go/common" 17 | fpeer "github.com/hyperledger/fabric-protos-go/peer" 18 | "github.com/hyperledger/fabric-protos-go/peer/lifecycle" 19 | ) 20 | 21 | // ApproveChaincodeDefinition approves a chaincode definition on a channel. 22 | func ApproveChaincodeDefinition(peers []*peer.Connection, o *orderer.Connection, channel string, sequence int64, name string, version string, packageID string) error { 23 | arg := &lifecycle.ApproveChaincodeDefinitionForMyOrgArgs{ 24 | Sequence: sequence, 25 | Name: name, 26 | Version: version, 27 | Source: &lifecycle.ChaincodeSource{ 28 | Type: &lifecycle.ChaincodeSource_LocalPackage{ 29 | LocalPackage: &lifecycle.ChaincodeSource_Local{ 30 | PackageId: packageID, 31 | }, 32 | }, 33 | }, 34 | } 35 | proposal, responses, endorsements, err := executeTransaction(peers, o, channel, "_lifecycle", "ApproveChaincodeDefinitionForMyOrg", util.MarshalOrPanic(arg)) 36 | if err != nil { 37 | return err 38 | } 39 | err = orderTransaction(peers, o, channel, proposal, responses, endorsements) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | // CommitChaincodeDefinition commits a chaincode definition on a channel. 47 | func CommitChaincodeDefinition(peers []*peer.Connection, o *orderer.Connection, channel string, sequence int64, name string, version string) error { 48 | arg := &lifecycle.CommitChaincodeDefinitionArgs{ 49 | Sequence: sequence, 50 | Name: name, 51 | Version: version, 52 | } 53 | proposal, responses, endorsements, err := executeTransaction(peers, o, channel, "_lifecycle", "CommitChaincodeDefinition", util.MarshalOrPanic(arg)) 54 | if err != nil { 55 | return err 56 | } 57 | err = orderTransaction(peers, o, channel, proposal, responses, endorsements) 58 | if err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | // EvaluateTransaction evaluates a transaction for a chaincode definition on a channel. 65 | func EvaluateTransaction(peers []*peer.Connection, o *orderer.Connection, channel, chaincode, function string, args ...string) ([]byte, error) { 66 | byteArgs := [][]byte{} 67 | for _, arg := range args { 68 | byteArgs = append(byteArgs, []byte(arg)) 69 | } 70 | _, responses, _, err := executeTransaction(peers, o, channel, chaincode, function, byteArgs...) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return responses[0].Response.Payload, nil 75 | } 76 | 77 | // SubmitTransaction submits a transaction for a chaincode definition on a channel. 78 | func SubmitTransaction(peers []*peer.Connection, o *orderer.Connection, channel, chaincode, function string, args ...string) ([]byte, error) { 79 | byteArgs := [][]byte{} 80 | for _, arg := range args { 81 | byteArgs = append(byteArgs, []byte(arg)) 82 | } 83 | proposal, responses, endorsements, err := executeTransaction(peers, o, channel, chaincode, function, byteArgs...) 84 | if err != nil { 85 | return nil, err 86 | } 87 | err = orderTransaction(peers, o, channel, proposal, responses, endorsements) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return responses[0].Response.Payload, nil 92 | } 93 | 94 | func executeTransaction(peers []*peer.Connection, o *orderer.Connection, channel, chaincode, function string, args ...[]byte) (*fpeer.Proposal, []*fpeer.ProposalResponse, []*fpeer.Endorsement, error) { 95 | firstPeer := peers[0] 96 | txID := txid.New(firstPeer.MSPID(), firstPeer.Identity()) 97 | channelHeader := protoutil.BuildChannelHeader(common.HeaderType_ENDORSER_TRANSACTION, channel, txID) 98 | cche := &fpeer.ChaincodeHeaderExtension{ 99 | ChaincodeId: &fpeer.ChaincodeID{ 100 | Name: chaincode, 101 | }, 102 | } 103 | channelHeader.Extension = util.MarshalOrPanic(cche) 104 | signatureHeader := protoutil.BuildSignatureHeader(txID) 105 | header := &common.Header{ 106 | ChannelHeader: util.MarshalOrPanic(channelHeader), 107 | SignatureHeader: util.MarshalOrPanic(signatureHeader), 108 | } 109 | cciSpec := &fpeer.ChaincodeInvocationSpec{ 110 | ChaincodeSpec: &fpeer.ChaincodeSpec{ 111 | Type: fpeer.ChaincodeSpec_GOLANG, 112 | ChaincodeId: &fpeer.ChaincodeID{ 113 | Name: chaincode, 114 | }, 115 | Input: &fpeer.ChaincodeInput{ 116 | Args: append([][]byte{[]byte(function)}, args...), 117 | }, 118 | }, 119 | } 120 | ccpp := &fpeer.ChaincodeProposalPayload{ 121 | Input: util.MarshalOrPanic(cciSpec), 122 | } 123 | proposal := &fpeer.Proposal{ 124 | Header: util.MarshalOrPanic(header), 125 | Payload: util.MarshalOrPanic(ccpp), 126 | } 127 | proposalBytes := util.MarshalOrPanic(proposal) 128 | signature := firstPeer.Identity().Sign(proposalBytes) 129 | signedProposal := &fpeer.SignedProposal{ 130 | ProposalBytes: proposalBytes, 131 | Signature: signature, 132 | } 133 | responses := []*fpeer.ProposalResponse{} 134 | endorsements := []*fpeer.Endorsement{} 135 | for _, peer := range peers { 136 | response, err := peer.ProcessProposal(signedProposal) 137 | if err != nil { 138 | return nil, nil, nil, err 139 | } else if response.Response.Status != int32(common.Status_SUCCESS) { 140 | return nil, nil, nil, fmt.Errorf("Bad proposal response: status %d, mesage %s", response.Response.Status, response.Response.Message) 141 | } 142 | responses = append(responses, response) 143 | endorsements = append(endorsements, response.Endorsement) 144 | } 145 | return proposal, responses, endorsements, nil 146 | } 147 | 148 | func orderTransaction(peers []*peer.Connection, o *orderer.Connection, channel string, proposal *fpeer.Proposal, responses []*fpeer.ProposalResponse, endorsements []*fpeer.Endorsement) error { 149 | firstPeer := peers[0] 150 | header := &common.Header{} 151 | util.UnmarshalOrPanic(proposal.Header, header) 152 | channelHeader := &common.ChannelHeader{} 153 | util.UnmarshalOrPanic(header.ChannelHeader, channelHeader) 154 | txID := channelHeader.TxId 155 | chaincodeEndorsedAction := &fpeer.ChaincodeEndorsedAction{ 156 | ProposalResponsePayload: responses[0].Payload, 157 | Endorsements: endorsements, 158 | } 159 | chaincodeActionPayload := &fpeer.ChaincodeActionPayload{ 160 | ChaincodeProposalPayload: proposal.Payload, 161 | Action: chaincodeEndorsedAction, 162 | } 163 | transactionAction := &fpeer.TransactionAction{ 164 | Header: header.SignatureHeader, 165 | Payload: util.MarshalOrPanic(chaincodeActionPayload), 166 | } 167 | transaction := &fpeer.Transaction{ 168 | Actions: []*fpeer.TransactionAction{ 169 | transactionAction, 170 | }, 171 | } 172 | payload := protoutil.BuildPayload(header, transaction) 173 | envelope := protoutil.BuildEnvelope(payload, o.Identity()) 174 | currentBlock, err := blocks.GetNewestBlock(firstPeer, channel) 175 | if err != nil { 176 | return err 177 | } 178 | nextBlockNumber := currentBlock.Header.Number + 1 179 | done := make(chan error) 180 | go func() { 181 | for { 182 | nextBlock, err := blocks.GetSpecificBlock(firstPeer, channel, nextBlockNumber) 183 | if err != nil { 184 | done <- err 185 | return 186 | } 187 | for _, data := range nextBlock.Data.Data { 188 | envelope := &common.Envelope{} 189 | util.UnmarshalOrPanic(data, envelope) 190 | payload := &common.Payload{} 191 | util.UnmarshalOrPanic(envelope.Payload, payload) 192 | channelHeader := &common.ChannelHeader{} 193 | util.UnmarshalOrPanic(payload.Header.ChannelHeader, channelHeader) 194 | if channelHeader.TxId == txID { 195 | done <- nil 196 | return 197 | } 198 | } 199 | nextBlockNumber++ 200 | } 201 | }() 202 | if err := o.Broadcast(envelope); err != nil { 203 | return err 204 | } 205 | err = <-done 206 | return err 207 | } 208 | -------------------------------------------------------------------------------- /internal/pkg/configtxlator/update.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. 2017 All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package configtxlator 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | 13 | "github.com/golang/protobuf/proto" 14 | cb "github.com/hyperledger/fabric-protos-go/common" 15 | "github.com/hyperledger/fabric/protoutil" 16 | ) 17 | 18 | func computePoliciesMapUpdate(original, updated map[string]*cb.ConfigPolicy) (readSet, writeSet, sameSet map[string]*cb.ConfigPolicy, updatedMembers bool) { 19 | readSet = make(map[string]*cb.ConfigPolicy) 20 | writeSet = make(map[string]*cb.ConfigPolicy) 21 | 22 | // All modified config goes into the read/write sets, but in case the map membership changes, we retain the 23 | // config which was the same to add to the read/write sets 24 | sameSet = make(map[string]*cb.ConfigPolicy) 25 | 26 | for policyName, originalPolicy := range original { 27 | updatedPolicy, ok := updated[policyName] 28 | if !ok { 29 | updatedMembers = true 30 | continue 31 | } 32 | 33 | if originalPolicy.ModPolicy == updatedPolicy.ModPolicy && proto.Equal(originalPolicy.Policy, updatedPolicy.Policy) { 34 | sameSet[policyName] = &cb.ConfigPolicy{ 35 | Version: originalPolicy.Version, 36 | } 37 | continue 38 | } 39 | 40 | writeSet[policyName] = &cb.ConfigPolicy{ 41 | Version: originalPolicy.Version + 1, 42 | ModPolicy: updatedPolicy.ModPolicy, 43 | Policy: updatedPolicy.Policy, 44 | } 45 | } 46 | 47 | for policyName, updatedPolicy := range updated { 48 | if _, ok := original[policyName]; ok { 49 | // If the updatedPolicy is in the original set of policies, it was already handled 50 | continue 51 | } 52 | updatedMembers = true 53 | writeSet[policyName] = &cb.ConfigPolicy{ 54 | Version: 0, 55 | ModPolicy: updatedPolicy.ModPolicy, 56 | Policy: updatedPolicy.Policy, 57 | } 58 | } 59 | 60 | return 61 | } 62 | 63 | func computeValuesMapUpdate(original, updated map[string]*cb.ConfigValue) (readSet, writeSet, sameSet map[string]*cb.ConfigValue, updatedMembers bool) { 64 | readSet = make(map[string]*cb.ConfigValue) 65 | writeSet = make(map[string]*cb.ConfigValue) 66 | 67 | // All modified config goes into the read/write sets, but in case the map membership changes, we retain the 68 | // config which was the same to add to the read/write sets 69 | sameSet = make(map[string]*cb.ConfigValue) 70 | 71 | for valueName, originalValue := range original { 72 | updatedValue, ok := updated[valueName] 73 | if !ok { 74 | updatedMembers = true 75 | continue 76 | } 77 | 78 | if originalValue.ModPolicy == updatedValue.ModPolicy && bytes.Equal(originalValue.Value, updatedValue.Value) { 79 | sameSet[valueName] = &cb.ConfigValue{ 80 | Version: originalValue.Version, 81 | } 82 | continue 83 | } 84 | 85 | writeSet[valueName] = &cb.ConfigValue{ 86 | Version: originalValue.Version + 1, 87 | ModPolicy: updatedValue.ModPolicy, 88 | Value: updatedValue.Value, 89 | } 90 | } 91 | 92 | for valueName, updatedValue := range updated { 93 | if _, ok := original[valueName]; ok { 94 | // If the updatedValue is in the original set of values, it was already handled 95 | continue 96 | } 97 | updatedMembers = true 98 | writeSet[valueName] = &cb.ConfigValue{ 99 | Version: 0, 100 | ModPolicy: updatedValue.ModPolicy, 101 | Value: updatedValue.Value, 102 | } 103 | } 104 | 105 | return 106 | } 107 | 108 | func computeGroupsMapUpdate(original, updated map[string]*cb.ConfigGroup) (readSet, writeSet, sameSet map[string]*cb.ConfigGroup, updatedMembers bool) { 109 | readSet = make(map[string]*cb.ConfigGroup) 110 | writeSet = make(map[string]*cb.ConfigGroup) 111 | 112 | // All modified config goes into the read/write sets, but in case the map membership changes, we retain the 113 | // config which was the same to add to the read/write sets 114 | sameSet = make(map[string]*cb.ConfigGroup) 115 | 116 | for groupName, originalGroup := range original { 117 | updatedGroup, ok := updated[groupName] 118 | if !ok { 119 | updatedMembers = true 120 | continue 121 | } 122 | 123 | groupReadSet, groupWriteSet, groupUpdated := computeGroupUpdate(originalGroup, updatedGroup) 124 | if !groupUpdated { 125 | sameSet[groupName] = groupReadSet 126 | continue 127 | } 128 | 129 | readSet[groupName] = groupReadSet 130 | writeSet[groupName] = groupWriteSet 131 | 132 | } 133 | 134 | for groupName, updatedGroup := range updated { 135 | if _, ok := original[groupName]; ok { 136 | // If the updatedGroup is in the original set of groups, it was already handled 137 | continue 138 | } 139 | updatedMembers = true 140 | _, groupWriteSet, _ := computeGroupUpdate(protoutil.NewConfigGroup(), updatedGroup) 141 | writeSet[groupName] = &cb.ConfigGroup{ 142 | Version: 0, 143 | ModPolicy: updatedGroup.ModPolicy, 144 | Policies: groupWriteSet.Policies, 145 | Values: groupWriteSet.Values, 146 | Groups: groupWriteSet.Groups, 147 | } 148 | } 149 | 150 | return 151 | } 152 | 153 | func computeGroupUpdate(original, updated *cb.ConfigGroup) (readSet, writeSet *cb.ConfigGroup, updatedGroup bool) { 154 | readSetPolicies, writeSetPolicies, sameSetPolicies, policiesMembersUpdated := computePoliciesMapUpdate(original.Policies, updated.Policies) 155 | readSetValues, writeSetValues, sameSetValues, valuesMembersUpdated := computeValuesMapUpdate(original.Values, updated.Values) 156 | readSetGroups, writeSetGroups, sameSetGroups, groupsMembersUpdated := computeGroupsMapUpdate(original.Groups, updated.Groups) 157 | 158 | // If the updated group is 'Equal' to the updated group (none of the members nor the mod policy changed) 159 | if !(policiesMembersUpdated || valuesMembersUpdated || groupsMembersUpdated || original.ModPolicy != updated.ModPolicy) { 160 | 161 | // If there were no modified entries in any of the policies/values/groups maps 162 | if len(readSetPolicies) == 0 && 163 | len(writeSetPolicies) == 0 && 164 | len(readSetValues) == 0 && 165 | len(writeSetValues) == 0 && 166 | len(readSetGroups) == 0 && 167 | len(writeSetGroups) == 0 { 168 | return &cb.ConfigGroup{ 169 | Version: original.Version, 170 | }, &cb.ConfigGroup{ 171 | Version: original.Version, 172 | }, false 173 | } 174 | 175 | return &cb.ConfigGroup{ 176 | Version: original.Version, 177 | Policies: readSetPolicies, 178 | Values: readSetValues, 179 | Groups: readSetGroups, 180 | }, &cb.ConfigGroup{ 181 | Version: original.Version, 182 | Policies: writeSetPolicies, 183 | Values: writeSetValues, 184 | Groups: writeSetGroups, 185 | }, true 186 | } 187 | 188 | for k, samePolicy := range sameSetPolicies { 189 | readSetPolicies[k] = samePolicy 190 | writeSetPolicies[k] = samePolicy 191 | } 192 | 193 | for k, sameValue := range sameSetValues { 194 | readSetValues[k] = sameValue 195 | writeSetValues[k] = sameValue 196 | } 197 | 198 | for k, sameGroup := range sameSetGroups { 199 | readSetGroups[k] = sameGroup 200 | writeSetGroups[k] = sameGroup 201 | } 202 | 203 | return &cb.ConfigGroup{ 204 | Version: original.Version, 205 | Policies: readSetPolicies, 206 | Values: readSetValues, 207 | Groups: readSetGroups, 208 | }, &cb.ConfigGroup{ 209 | Version: original.Version + 1, 210 | Policies: writeSetPolicies, 211 | Values: writeSetValues, 212 | Groups: writeSetGroups, 213 | ModPolicy: updated.ModPolicy, 214 | }, true 215 | } 216 | 217 | // Compute computes a config update between the specified original and updated config. 218 | func Compute(original, updated *cb.Config) (*cb.ConfigUpdate, error) { 219 | if original.ChannelGroup == nil { 220 | return nil, fmt.Errorf("no channel group included for original config") 221 | } 222 | 223 | if updated.ChannelGroup == nil { 224 | return nil, fmt.Errorf("no channel group included for updated config") 225 | } 226 | 227 | readSet, writeSet, groupUpdated := computeGroupUpdate(original.ChannelGroup, updated.ChannelGroup) 228 | if !groupUpdated { 229 | return nil, fmt.Errorf("no differences detected between original and updated config") 230 | } 231 | return &cb.ConfigUpdate{ 232 | ReadSet: readSet, 233 | WriteSet: writeSet, 234 | }, nil 235 | } 236 | -------------------------------------------------------------------------------- /internal/pkg/channel/channel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package channel 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/gogo/protobuf/proto" 11 | "github.com/hyperledger-labs/microfab/internal/pkg/config" 12 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/orderer" 14 | "github.com/hyperledger-labs/microfab/internal/pkg/protoutil" 15 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 16 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 17 | "github.com/hyperledger/fabric-protos-go/common" 18 | fmsp "github.com/hyperledger/fabric-protos-go/msp" 19 | "github.com/hyperledger/fabric-protos-go/peer" 20 | ) 21 | 22 | type channelOperation struct { 23 | config *common.Config 24 | mspID string 25 | identity *identity.Identity 26 | } 27 | 28 | // Option is a type representing an option for creating or updating a channel. 29 | type Option func(*channelOperation) error 30 | 31 | // AddMSPID adds the specified MSP ID to the channel. 32 | func AddMSPID(mspID string) Option { 33 | return func(operation *channelOperation) error { 34 | operation.config.GetChannelGroup().Groups["Application"].Groups[mspID] = &common.ConfigGroup{} 35 | return nil 36 | } 37 | } 38 | 39 | // AddMSPIDs adds the specified MSP IDs to the channel. 40 | func AddMSPIDs(mspIDs ...string) Option { 41 | return func(operation *channelOperation) error { 42 | for _, mspID := range mspIDs { 43 | err := AddMSPID(mspID)(operation) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | } 51 | 52 | // AddAnchorPeer adds the specified anchor peer to the channel. 53 | func AddAnchorPeer(mspID string, hostname string, port int32) Option { 54 | return func(operation *channelOperation) error { 55 | msp, ok := operation.config.GetChannelGroup().Groups["Application"].Groups[mspID] 56 | if !ok { 57 | return fmt.Errorf("The channel does not contain an MSP with ID %s", mspID) 58 | } 59 | cv, ok := msp.Values["AnchorPeers"] 60 | if !ok { 61 | msp.Values["AnchorPeers"] = &common.ConfigValue{ 62 | ModPolicy: "Admins", 63 | Value: util.MarshalOrPanic(&peer.AnchorPeers{ 64 | AnchorPeers: []*peer.AnchorPeer{}, 65 | }), 66 | } 67 | cv = msp.Values["AnchorPeers"] 68 | } 69 | aps := &peer.AnchorPeers{} 70 | proto.Unmarshal(cv.Value, aps) 71 | aps.AnchorPeers = append(aps.AnchorPeers, &peer.AnchorPeer{ 72 | Host: hostname, 73 | Port: port, 74 | }) 75 | cv.Value = util.MarshalOrPanic(aps) 76 | return nil 77 | } 78 | } 79 | 80 | // WithCapabilityLevel set the specified capability level for the channel. 81 | func WithCapabilityLevel(capabilityLevel string) Option { 82 | return func(operation *channelOperation) error { 83 | operation.config.GetChannelGroup().Groups["Application"].Values["Capabilities"].Value = util.MarshalOrPanic(&common.Capabilities{ 84 | Capabilities: map[string]*common.Capability{ 85 | capabilityLevel: {}, 86 | }, 87 | }) 88 | return nil 89 | } 90 | } 91 | 92 | // UsingMSPID uses the specified MSP ID to create or update the channel. 93 | func UsingMSPID(mspID string) Option { 94 | return func(operation *channelOperation) error { 95 | operation.mspID = mspID 96 | return nil 97 | } 98 | } 99 | 100 | // UsingIdentity uses the specified identity to create or update the channel. 101 | func UsingIdentity(identity *identity.Identity) Option { 102 | return func(operation *channelOperation) error { 103 | operation.identity = identity 104 | return nil 105 | } 106 | } 107 | 108 | // CreateChannel creates a new channel on the specified ordering service. 109 | func CreateChannel(o *orderer.Connection, channel string, opts ...Option) error { 110 | _ = &common.Policy{ 111 | Type: int32(common.Policy_SIGNATURE), 112 | Value: util.MarshalOrPanic(&common.SignaturePolicyEnvelope{ 113 | Identities: []*fmsp.MSPPrincipal{}, 114 | Rule: &common.SignaturePolicy{ 115 | Type: &common.SignaturePolicy_NOutOf_{ 116 | NOutOf: &common.SignaturePolicy_NOutOf{ 117 | N: 1, 118 | Rules: []*common.SignaturePolicy{}, 119 | }, 120 | }, 121 | }, 122 | }), 123 | } 124 | configUpdate := &common.ConfigUpdate{ 125 | ChannelId: channel, 126 | ReadSet: &common.ConfigGroup{ 127 | Groups: map[string]*common.ConfigGroup{ 128 | "Application": { 129 | Groups: map[string]*common.ConfigGroup{}, 130 | }, 131 | }, 132 | Values: map[string]*common.ConfigValue{ 133 | "Consortium": { 134 | Value: util.MarshalOrPanic(&common.Consortium{ 135 | Name: "SampleConsortium", 136 | }), 137 | }, 138 | }, 139 | }, 140 | WriteSet: &common.ConfigGroup{ 141 | Groups: map[string]*common.ConfigGroup{ 142 | "Application": { 143 | Groups: map[string]*common.ConfigGroup{}, 144 | ModPolicy: "Admins", 145 | Policies: map[string]*common.ConfigPolicy{ 146 | "Admins": protoutil.BuildImplicitMetaConfigPolicy(common.ImplicitMetaPolicy_ANY, "Admins"), 147 | "Endorsement": protoutil.BuildImplicitMetaConfigPolicy(common.ImplicitMetaPolicy_ANY, "Endorsement"), 148 | "LifecycleEndorsement": protoutil.BuildImplicitMetaConfigPolicy(common.ImplicitMetaPolicy_ANY, "Endorsement"), 149 | "Readers": protoutil.BuildImplicitMetaConfigPolicy(common.ImplicitMetaPolicy_ANY, "Readers"), 150 | "Writers": protoutil.BuildImplicitMetaConfigPolicy(common.ImplicitMetaPolicy_ANY, "Writers"), 151 | }, 152 | Values: map[string]*common.ConfigValue{ 153 | "Capabilities": { 154 | ModPolicy: "Admins", 155 | Value: util.MarshalOrPanic(&common.Capabilities{ 156 | Capabilities: map[string]*common.Capability{ 157 | "V2_5": {}, 158 | }, 159 | }), 160 | }, 161 | }, 162 | Version: 1, 163 | }, 164 | }, 165 | Values: map[string]*common.ConfigValue{ 166 | "Consortium": { 167 | Value: util.MarshalOrPanic(&common.Consortium{ 168 | Name: "SampleConsortium", 169 | }), 170 | }, 171 | }, 172 | }, 173 | } 174 | operation := &channelOperation{ 175 | &common.Config{ 176 | ChannelGroup: configUpdate.WriteSet, 177 | }, 178 | o.MSPID(), 179 | o.Identity(), 180 | } 181 | for _, opt := range opts { 182 | err := opt(operation) 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | for mspID := range configUpdate.WriteSet.Groups["Application"].Groups { 188 | configUpdate.ReadSet.Groups["Application"].Groups[mspID] = &common.ConfigGroup{} 189 | } 190 | return createOrUpdateChannel(o, operation.mspID, operation.identity, configUpdate) 191 | } 192 | 193 | // UpdateChannel updates an existing channel on the specified ordering service. 194 | func UpdateChannel(o *orderer.Connection, channel string, opts ...Option) error { 195 | originalConfig, err := config.GetConfig(o, channel) 196 | if err != nil { 197 | return err 198 | } 199 | newConfig := proto.Clone(originalConfig).(*common.Config) 200 | operation := &channelOperation{ 201 | newConfig, 202 | o.MSPID(), 203 | o.Identity(), 204 | } 205 | for _, opt := range opts { 206 | err := opt(operation) 207 | if err != nil { 208 | return err 209 | } 210 | } 211 | configUpdate, err := config.GenerateConfigUpdate(originalConfig, newConfig) 212 | if err != nil { 213 | return err 214 | } 215 | configUpdate.ChannelId = channel 216 | return createOrUpdateChannel(o, operation.mspID, operation.identity, configUpdate) 217 | } 218 | 219 | func createOrUpdateChannel(o *orderer.Connection, mspID string, identity *identity.Identity, configUpdate *common.ConfigUpdate) error { 220 | txID := txid.New(mspID, identity) 221 | header := protoutil.BuildHeader(common.HeaderType_CONFIG_UPDATE, configUpdate.ChannelId, txID) 222 | configUpdateBytes := util.MarshalOrPanic(configUpdate) 223 | signature := identity.Sign(header.SignatureHeader, configUpdateBytes) 224 | configUpdateEnvelope := &common.ConfigUpdateEnvelope{ 225 | ConfigUpdate: configUpdateBytes, 226 | Signatures: []*common.ConfigSignature{ 227 | { 228 | SignatureHeader: header.SignatureHeader, 229 | Signature: signature, 230 | }, 231 | }, 232 | } 233 | payload := protoutil.BuildPayload(header, configUpdateEnvelope) 234 | envelope := protoutil.BuildEnvelope(payload, o.Identity()) 235 | err := o.Broadcast(envelope) 236 | if err != nil { 237 | return err 238 | } 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /internal/pkg/protoutil/protoutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package protoutil 6 | 7 | import ( 8 | "crypto/sha256" 9 | "time" 10 | 11 | "github.com/golang/protobuf/proto" 12 | "github.com/golang/protobuf/ptypes/timestamp" 13 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 14 | "github.com/hyperledger-labs/microfab/internal/pkg/organization" 15 | "github.com/hyperledger-labs/microfab/internal/pkg/txid" 16 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 17 | "github.com/hyperledger/fabric-protos-go/common" 18 | "github.com/hyperledger/fabric-protos-go/msp" 19 | ) 20 | 21 | // GenerateTimestamp generates a new timestamp with the current time. 22 | func GenerateTimestamp() *timestamp.Timestamp { 23 | now := time.Now() 24 | seconds := now.Unix() 25 | nanos := int32(now.UnixNano() - (seconds * 1000000000)) 26 | return ×tamp.Timestamp{ 27 | Seconds: seconds, 28 | Nanos: nanos, 29 | } 30 | } 31 | 32 | // BuildChannelHeader builds a channel header for the specified channel and transaction ID. 33 | func BuildChannelHeader(headerType common.HeaderType, channel string, txID *txid.TransactionID) *common.ChannelHeader { 34 | timestamp := GenerateTimestamp() 35 | return &common.ChannelHeader{ 36 | Type: int32(headerType), 37 | Version: 1, 38 | ChannelId: channel, 39 | TxId: txID.String(), 40 | Timestamp: timestamp, 41 | TlsCertHash: txID.Identity().Certificate().Hash(), 42 | } 43 | } 44 | 45 | // BuildSignatureHeader builds a signature header for the specified transaction ID. 46 | func BuildSignatureHeader(txID *txid.TransactionID) *common.SignatureHeader { 47 | serializedIdentity := &msp.SerializedIdentity{ 48 | Mspid: txID.MSPID(), 49 | IdBytes: txID.Identity().Certificate().Bytes(), 50 | } 51 | return &common.SignatureHeader{ 52 | Creator: util.MarshalOrPanic(serializedIdentity), 53 | Nonce: txID.Nonce(), 54 | } 55 | } 56 | 57 | // BuildHeader builds a header for the specified header type, channel, and transaction ID. 58 | func BuildHeader(headerType common.HeaderType, channel string, txID *txid.TransactionID) *common.Header { 59 | channelHeader := BuildChannelHeader(headerType, channel, txID) 60 | signatureHeader := BuildSignatureHeader(txID) 61 | return &common.Header{ 62 | ChannelHeader: util.MarshalOrPanic(channelHeader), 63 | SignatureHeader: util.MarshalOrPanic(signatureHeader), 64 | } 65 | } 66 | 67 | // BuildPayload builds a payload for the specified header and data. 68 | func BuildPayload(header *common.Header, data proto.Message) *common.Payload { 69 | return &common.Payload{ 70 | Header: header, 71 | Data: util.MarshalOrPanic(data), 72 | } 73 | } 74 | 75 | // BuildEnvelope builds an envelope for the specified payload and signs it. 76 | func BuildEnvelope(payload *common.Payload, identity *identity.Identity) *common.Envelope { 77 | payloadBytes := util.MarshalOrPanic(payload) 78 | signature := identity.Sign(payloadBytes) 79 | return &common.Envelope{ 80 | Payload: payloadBytes, 81 | Signature: signature, 82 | } 83 | } 84 | 85 | // BuildGenesisBlock builds a genesis block containing the specified envelope. 86 | func BuildGenesisBlock(envelope *common.Envelope) *common.Block { 87 | data := util.MarshalOrPanic(envelope) 88 | dataHash := sha256.Sum256(data) 89 | blockHeader := &common.BlockHeader{ 90 | Number: 0, 91 | PreviousHash: nil, 92 | DataHash: dataHash[:], 93 | } 94 | blockMetadata := &common.BlockMetadata{ 95 | Metadata: [][]byte{ 96 | {}, 97 | {}, 98 | {}, 99 | {}, 100 | {}, 101 | }, 102 | } 103 | blockData := &common.BlockData{ 104 | Data: [][]byte{ 105 | data, 106 | }, 107 | } 108 | return &common.Block{ 109 | Header: blockHeader, 110 | Metadata: blockMetadata, 111 | Data: blockData, 112 | } 113 | } 114 | 115 | // BuildImplicitMetaPolicy builds an implicit meta policy for the specified rule and sub policy. 116 | func BuildImplicitMetaPolicy(rule common.ImplicitMetaPolicy_Rule, subPolicy string) *common.Policy { 117 | return &common.Policy{ 118 | Type: int32(common.Policy_IMPLICIT_META), 119 | Value: util.MarshalOrPanic(&common.ImplicitMetaPolicy{ 120 | Rule: rule, 121 | SubPolicy: subPolicy, 122 | }), 123 | } 124 | } 125 | 126 | // BuildImplicitMetaConfigPolicy builds an implicit meta config policy for the specified rule and subpolicy. 127 | func BuildImplicitMetaConfigPolicy(rule common.ImplicitMetaPolicy_Rule, subPolicy string) *common.ConfigPolicy { 128 | return &common.ConfigPolicy{ 129 | ModPolicy: "Admins", 130 | Policy: BuildImplicitMetaPolicy(rule, subPolicy), 131 | } 132 | } 133 | 134 | // BuildSignaturePolicyEnvelope builds a signature policy envelope for the specified MSP ID and role. 135 | func BuildSignaturePolicyEnvelope(mspID string, role msp.MSPRole_MSPRoleType) *common.SignaturePolicyEnvelope { 136 | return &common.SignaturePolicyEnvelope{ 137 | Identities: []*msp.MSPPrincipal{ 138 | { 139 | PrincipalClassification: msp.MSPPrincipal_ROLE, 140 | Principal: util.MarshalOrPanic(&msp.MSPRole{ 141 | MspIdentifier: mspID, 142 | Role: role, 143 | }), 144 | }, 145 | }, 146 | Rule: &common.SignaturePolicy{ 147 | Type: &common.SignaturePolicy_NOutOf_{ 148 | NOutOf: &common.SignaturePolicy_NOutOf{ 149 | N: 1, 150 | Rules: []*common.SignaturePolicy{ 151 | { 152 | Type: &common.SignaturePolicy_SignedBy{ 153 | SignedBy: 0, 154 | }, 155 | }, 156 | }, 157 | }, 158 | }, 159 | }, 160 | } 161 | } 162 | 163 | // BuildFabricCryptoConfig builds the default Fabric crypto configuration. 164 | func BuildFabricCryptoConfig() *msp.FabricCryptoConfig { 165 | return &msp.FabricCryptoConfig{ 166 | IdentityIdentifierHashFunction: "SHA256", 167 | SignatureHashFamily: "SHA2", 168 | } 169 | } 170 | 171 | // BuildFabricMSPConfig builds the Fabric MSP configuration for an organization. 172 | func BuildFabricMSPConfig(organization *organization.Organization, tls *identity.Identity) (*msp.FabricMSPConfig, error) { 173 | mspConfig := &msp.FabricMSPConfig{ 174 | Name: organization.MSPID(), 175 | RootCerts: [][]byte{ 176 | organization.CA().Certificate().Bytes(), 177 | }, 178 | IntermediateCerts: [][]byte{}, 179 | TlsRootCerts: [][]byte{}, 180 | TlsIntermediateCerts: [][]byte{}, 181 | Admins: [][]byte{ 182 | organization.Admin().Certificate().Bytes(), 183 | }, 184 | RevocationList: [][]byte{}, 185 | SigningIdentity: nil, 186 | OrganizationalUnitIdentifiers: []*msp.FabricOUIdentifier{}, 187 | CryptoConfig: BuildFabricCryptoConfig(), 188 | FabricNodeOus: BuildFabricNodeOUs(), 189 | } 190 | if tls != nil { 191 | mspConfig.TlsRootCerts = [][]byte{tls.CA().Bytes()} 192 | } 193 | return mspConfig, nil 194 | } 195 | 196 | // BuildFabricNodeOUs builds the default Fabric NodeOU configuration. 197 | func BuildFabricNodeOUs() *msp.FabricNodeOUs { 198 | return &msp.FabricNodeOUs{ 199 | Enable: true, 200 | AdminOuIdentifier: &msp.FabricOUIdentifier{ 201 | OrganizationalUnitIdentifier: "admin", 202 | }, 203 | ClientOuIdentifier: &msp.FabricOUIdentifier{ 204 | OrganizationalUnitIdentifier: "client", 205 | }, 206 | PeerOuIdentifier: &msp.FabricOUIdentifier{ 207 | OrganizationalUnitIdentifier: "peer", 208 | }, 209 | OrdererOuIdentifier: &msp.FabricOUIdentifier{ 210 | OrganizationalUnitIdentifier: "orderer", 211 | }, 212 | } 213 | } 214 | 215 | // BuildConfigGroupFromOrganization builds a config group from an organization. 216 | func BuildConfigGroupFromOrganization(organization *organization.Organization, tls *identity.Identity) (*common.ConfigGroup, error) { 217 | signaturePolicyEnvelope := BuildSignaturePolicyEnvelope(organization.MSPID(), msp.MSPRole_MEMBER) 218 | configPolicy := &common.ConfigPolicy{ 219 | ModPolicy: "Admins", 220 | Policy: &common.Policy{ 221 | Type: int32(common.Policy_SIGNATURE), 222 | Value: util.MarshalOrPanic(signaturePolicyEnvelope), 223 | }, 224 | } 225 | fabricMSPConfig, err := BuildFabricMSPConfig(organization, tls) 226 | if err != nil { 227 | return nil, err 228 | } 229 | configGroup := &common.ConfigGroup{ 230 | Groups: map[string]*common.ConfigGroup{}, 231 | ModPolicy: "/Channel/Application/Admins", 232 | Policies: map[string]*common.ConfigPolicy{ 233 | "Admins": configPolicy, 234 | "Readers": configPolicy, 235 | "Writers": configPolicy, 236 | "Endorsement": configPolicy, 237 | }, 238 | Values: map[string]*common.ConfigValue{ 239 | "MSP": { 240 | ModPolicy: "Admins", 241 | Value: util.MarshalOrPanic(&msp.MSPConfig{ 242 | Type: 0, 243 | Config: util.MarshalOrPanic(fabricMSPConfig), 244 | }), 245 | }, 246 | }, 247 | } 248 | return configGroup, nil 249 | } 250 | -------------------------------------------------------------------------------- /internal/pkg/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package proxy 6 | 7 | import ( 8 | "crypto/tls" 9 | gotls "crypto/tls" 10 | "crypto/x509" 11 | "fmt" 12 | "log" 13 | "net" 14 | "net/http" 15 | "net/http/httputil" 16 | "os" 17 | "regexp" 18 | 19 | "github.com/hyperledger-labs/microfab/internal/pkg/ca" 20 | "github.com/hyperledger-labs/microfab/internal/pkg/console" 21 | "github.com/hyperledger-labs/microfab/internal/pkg/couchdb" 22 | "github.com/hyperledger-labs/microfab/internal/pkg/identity" 23 | "github.com/hyperledger-labs/microfab/internal/pkg/orderer" 24 | "github.com/hyperledger-labs/microfab/internal/pkg/peer" 25 | "golang.org/x/net/http2" 26 | "golang.org/x/net/http2/h2c" 27 | ) 28 | 29 | var logger = log.New(os.Stdout, fmt.Sprintf("[%16s] ", "console"), log.LstdFlags) 30 | 31 | type route struct { 32 | SourceHost string 33 | TargetHost string 34 | UseHTTP2 bool 35 | UseTLS bool 36 | } 37 | 38 | type routeMap map[string]*route 39 | 40 | // Proxy represents an instance of a proxy. 41 | type Proxy struct { 42 | httpServer *http.Server 43 | routes []*route 44 | routeMap routeMap 45 | tls *identity.Identity 46 | caCertPool *x509.CertPool 47 | peerCert gotls.Certificate 48 | } 49 | 50 | type h2cTransportWrapper struct { 51 | *http2.Transport 52 | } 53 | 54 | func (tw *h2cTransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) { 55 | req.URL.Scheme = "http" 56 | return tw.Transport.RoundTrip(req) 57 | } 58 | 59 | var portRegex = regexp.MustCompile(":\\d+$") 60 | 61 | // New creates a new instance of a proxy. 62 | func New(port int) (*Proxy, error) { 63 | p := &Proxy{routeMap: routeMap{}} 64 | director := func(req *http.Request) { 65 | host := req.Host 66 | if !portRegex.MatchString(host) { 67 | host += fmt.Sprintf(":%d", port) 68 | } 69 | route, ok := p.routeMap[host] 70 | if !ok && len(p.routes) > 0 { 71 | route = p.routes[0] 72 | logger.Printf("No route found for '%s' assuming ['%s','%s']", host, route.SourceHost, route.TargetHost) 73 | } 74 | logger.Printf("Using route mapping for '%s' ['%s','%s','%t']", host, route.SourceHost, route.TargetHost, route.UseTLS) 75 | if route.UseHTTP2 { 76 | req.URL.Scheme = "h2c" 77 | } else { 78 | req.URL.Scheme = "http" 79 | } 80 | req.URL.Host = route.TargetHost 81 | } 82 | httpTransport := &http.Transport{} 83 | httpTransport.RegisterProtocol("h2c", &h2cTransportWrapper{ 84 | Transport: &http2.Transport{ 85 | AllowHTTP: true, 86 | DialTLS: func(network, addr string, cfg *gotls.Config) (net.Conn, error) { 87 | return net.Dial(network, addr) 88 | }, 89 | }, 90 | }) 91 | err := http2.ConfigureTransport(httpTransport) 92 | if err != nil { 93 | return nil, err 94 | } 95 | reverseProxy := &httputil.ReverseProxy{ 96 | Director: director, 97 | Transport: httpTransport, 98 | FlushInterval: -1, 99 | } 100 | httpServer := &http.Server{ 101 | Addr: fmt.Sprintf(":%d", port), 102 | Handler: h2c.NewHandler(reverseProxy, &http2.Server{}), 103 | } 104 | err = http2.ConfigureServer(httpServer, nil) 105 | if err != nil { 106 | return nil, err 107 | } 108 | p.httpServer = httpServer 109 | return p, nil 110 | } 111 | 112 | // NewWithTLS creates a new instance of a proxy that is TLS enabled. 113 | func NewWithTLS(tls *identity.Identity, port int) (*Proxy, error) { 114 | p := &Proxy{routeMap: routeMap{}, tls: tls} 115 | p.caCertPool = x509.NewCertPool() 116 | p.caCertPool.AddCert(tls.CA().Certificate()) 117 | 118 | director := func(req *http.Request) { 119 | host := req.Host 120 | logger.Printf("RemoteAddr=%s RequestURI=%s Host=%s", req.RemoteAddr, req.RequestURI, host) 121 | if !portRegex.MatchString(host) { 122 | host += fmt.Sprintf(":%d", port) 123 | } 124 | route, ok := p.routeMap[host] 125 | if !ok && len(p.routes) > 0 { 126 | route = p.routes[0] 127 | logger.Printf("No route found for '%s' assuming ['%s','%s']", host, route.SourceHost, route.TargetHost) 128 | } 129 | logger.Printf("Using route mapping for '%s' ['%s','%s','%t','%t']", host, route.SourceHost, route.TargetHost, route.UseTLS, route.UseHTTP2) 130 | if route.UseTLS { 131 | req.URL.Scheme = "https" 132 | } else { 133 | req.URL.Scheme = "http" 134 | } 135 | req.URL.Host = route.TargetHost 136 | } 137 | 138 | // have attempted to ensure the TLS Cert is passed through. Used a customer 'dialTLS' implementation 139 | // but didn't seem to really work 140 | // DialTLS: p.dialTLS, 141 | 142 | httpTransport := &http.Transport{ 143 | TLSClientConfig: &gotls.Config{ 144 | InsecureSkipVerify: true, 145 | }, 146 | // DialTLS: p.dialTLS, 147 | ForceAttemptHTTP2: true, 148 | } 149 | err := http2.ConfigureTransport(httpTransport) 150 | if err != nil { 151 | return nil, err 152 | } 153 | reverseProxy := &httputil.ReverseProxy{ 154 | Director: director, 155 | Transport: httpTransport, 156 | FlushInterval: -1, 157 | } 158 | httpServer := &http.Server{ 159 | Addr: fmt.Sprintf(":%d", port), 160 | Handler: reverseProxy, 161 | } 162 | err = http2.ConfigureServer(httpServer, nil) 163 | if err != nil { 164 | return nil, err 165 | } 166 | if err != nil { 167 | return nil, err 168 | } 169 | certificate, err := gotls.X509KeyPair(tls.Certificate().Bytes(), tls.PrivateKey().Bytes()) 170 | p.peerCert = certificate 171 | if err != nil { 172 | return nil, err 173 | } 174 | httpServer.TLSConfig = &gotls.Config{ 175 | NextProtos: []string{ 176 | "h2", 177 | "http/1.1", 178 | }, 179 | Certificates: []gotls.Certificate{certificate}, 180 | } 181 | p.httpServer = httpServer 182 | return p, nil 183 | } 184 | 185 | func (p *Proxy) dialTLS(network, addr string) (net.Conn, error) { 186 | logger.Printf("dialTLS %s %s", network, addr) 187 | conn, err := net.Dial(network, addr) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | // host, _, err := net.SplitHostPort(addr) 193 | // if err != nil { 194 | // return nil, err 195 | // } 196 | cfg := &tls.Config{ 197 | RootCAs: p.caCertPool, 198 | Certificates: []tls.Certificate{p.peerCert}, 199 | InsecureSkipVerify: true, 200 | } 201 | 202 | tlsConn := tls.Client(conn, cfg) 203 | if err := tlsConn.Handshake(); err != nil { 204 | conn.Close() 205 | return nil, err 206 | } 207 | 208 | cs := tlsConn.ConnectionState() 209 | cert := cs.PeerCertificates[0] 210 | 211 | // Verify here 212 | log.Println(cert.Subject) 213 | 214 | return tlsConn, nil 215 | } 216 | 217 | // RegisterConsole registers the specified console with the proxy. 218 | func (p *Proxy) RegisterConsole(console *console.Console) { 219 | route := &route{ 220 | SourceHost: console.URL().Host, 221 | TargetHost: fmt.Sprintf("localhost:%d", console.Port()), 222 | UseHTTP2: false, 223 | UseTLS: true, 224 | } 225 | p.routes = append(p.routes, route) 226 | p.buildRouteMap() 227 | } 228 | 229 | // RegisterPeer registers the specified peer with the proxy. 230 | func (p *Proxy) RegisterPeer(peer *peer.Peer) { 231 | routes := []*route{ 232 | { 233 | SourceHost: peer.APIHost(false), 234 | TargetHost: peer.APIHost(true), 235 | UseHTTP2: true, 236 | UseTLS: true, 237 | }, 238 | { 239 | SourceHost: peer.ChaincodeHost(false), 240 | TargetHost: peer.ChaincodeHost(true), 241 | UseHTTP2: true, 242 | UseTLS: true, 243 | }, 244 | { 245 | SourceHost: peer.OperationsHost(false), 246 | TargetHost: peer.OperationsHost(true), 247 | UseHTTP2: false, 248 | UseTLS: true, 249 | }, 250 | } 251 | p.routes = append(p.routes, routes...) 252 | p.buildRouteMap() 253 | } 254 | 255 | // RegisterOrderer registers the specified orderer with the proxy. 256 | func (p *Proxy) RegisterOrderer(orderer *orderer.Orderer) { 257 | routes := []*route{ 258 | { 259 | SourceHost: orderer.APIHost(false), 260 | TargetHost: orderer.APIHost(true), 261 | UseHTTP2: true, 262 | UseTLS: true, 263 | }, 264 | { 265 | SourceHost: orderer.OperationsHost(false), 266 | TargetHost: orderer.OperationsHost(true), 267 | UseHTTP2: false, 268 | UseTLS: true, 269 | }, 270 | } 271 | p.routes = append(p.routes, routes...) 272 | p.buildRouteMap() 273 | } 274 | 275 | // RegisterCA registers the specified CA with the proxy. 276 | func (p *Proxy) RegisterCA(ca *ca.CA) { 277 | routes := []*route{ 278 | { 279 | SourceHost: ca.APIHost(false), 280 | TargetHost: ca.APIHost(true), 281 | UseHTTP2: false, 282 | UseTLS: true, 283 | }, 284 | { 285 | SourceHost: ca.OperationsHost(false), 286 | TargetHost: ca.OperationsHost(true), 287 | UseHTTP2: false, 288 | UseTLS: true, 289 | }, 290 | } 291 | p.routes = append(p.routes, routes...) 292 | p.buildRouteMap() 293 | } 294 | 295 | // RegisterCouchDB registers the specified CouchDB with the proxy. 296 | func (p *Proxy) RegisterCouchDB(couchDB couchdb.CouchDB) { 297 | route := &route{ 298 | SourceHost: couchDB.URL(false).Host, 299 | TargetHost: couchDB.URL(true).Host, 300 | UseHTTP2: false, 301 | UseTLS: false, 302 | } 303 | p.routes = append(p.routes, route) 304 | p.buildRouteMap() 305 | } 306 | 307 | // Start starts the proxy. 308 | func (p *Proxy) Start() error { 309 | if p.tls != nil { 310 | return p.httpServer.ListenAndServeTLS("", "") 311 | } 312 | return p.httpServer.ListenAndServe() 313 | } 314 | 315 | // Stop stops the proxy. 316 | func (p *Proxy) Stop() error { 317 | return p.httpServer.Close() 318 | } 319 | 320 | func (p *Proxy) buildRouteMap() { 321 | for _, route := range p.routes { 322 | p.routeMap[route.SourceHost] = route 323 | } 324 | } 325 | 326 | // DumpRouteMap logs the route mappings that have been registered 327 | func (p *Proxy) DumpRouteMap() { 328 | for key, route := range p.routeMap { 329 | logger.Printf("%s ==> %v \n", key, route) 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /internal/pkg/peer/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 3 | */ 4 | 5 | package peer 6 | 7 | import ( 8 | "bufio" 9 | "crypto/tls" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "path" 17 | "strings" 18 | "time" 19 | 20 | "github.com/hyperledger-labs/microfab/internal/pkg/util" 21 | "github.com/pkg/errors" 22 | "gopkg.in/yaml.v2" 23 | ) 24 | 25 | var logger = log.New(os.Stdout, fmt.Sprintf("[%16s] ", "console"), log.LstdFlags) 26 | 27 | // Start starts the peer. 28 | func (p *Peer) Start(timeout time.Duration) error { 29 | err := p.createDirectories() 30 | if err != nil { 31 | return err 32 | } 33 | configDirectory := path.Join(p.directory, "config") 34 | dataDirectory := path.Join(p.directory, "data") 35 | logsDirectory := path.Join(p.directory, "logs") 36 | mspDirectory := path.Join(p.directory, "msp") 37 | err = util.CreateMSPDirectory(mspDirectory, p.identity) 38 | if err != nil { 39 | return err 40 | } 41 | err = p.createConfig(dataDirectory, mspDirectory) 42 | if err != nil { 43 | return err 44 | } 45 | cmd := exec.Command("peer", "node", "start") 46 | cmd.Env = os.Environ() 47 | extraEnvs := []string{ 48 | fmt.Sprintf("FABRIC_CFG_PATH=%s", configDirectory), 49 | } 50 | cmd.Env = append(cmd.Env, extraEnvs...) 51 | cmd.Stdin = nil 52 | logFile, err := os.OpenFile(path.Join(logsDirectory, "peer.log"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 53 | if err != nil { 54 | return err 55 | } 56 | pipe, err := cmd.StdoutPipe() 57 | if err != nil { 58 | return err 59 | } 60 | go func() { 61 | reader := bufio.NewReader(pipe) 62 | scanner := bufio.NewScanner(reader) 63 | scanner.Split(bufio.ScanLines) 64 | id := strings.ToLower(p.identity.Name()) 65 | id = strings.ReplaceAll(id, " ", "") 66 | logger := log.New(os.Stdout, fmt.Sprintf("[%16s] ", id), 0) 67 | for scanner.Scan() { 68 | logger.Println(scanner.Text()) 69 | logFile.WriteString(scanner.Text()) 70 | } 71 | pipe.Close() 72 | logFile.Close() 73 | }() 74 | cmd.Stderr = cmd.Stdout 75 | err = cmd.Start() 76 | if err != nil { 77 | return err 78 | } 79 | p.command = cmd 80 | errchan := make(chan error, 1) 81 | go func() { 82 | err = cmd.Wait() 83 | if err != nil { 84 | errchan <- err 85 | } 86 | }() 87 | timeoutCh := time.After(timeout) 88 | tick := time.Tick(250 * time.Millisecond) 89 | for { 90 | select { 91 | case <-timeoutCh: 92 | p.Stop() 93 | return errors.New("timeout whilst waiting for peer to start") 94 | case err := <-errchan: 95 | p.Stop() 96 | return errors.WithMessage(err, "failed to start peer") 97 | case <-tick: 98 | if p.hasStarted() { 99 | return nil 100 | } 101 | } 102 | } 103 | } 104 | 105 | // Stop stops the peer. 106 | func (p *Peer) Stop() error { 107 | if p.command != nil { 108 | err := p.command.Process.Kill() 109 | if err != nil { 110 | return errors.WithMessage(err, "failed to stop peer") 111 | } 112 | p.command = nil 113 | } 114 | return nil 115 | } 116 | 117 | func (p *Peer) createDirectories() error { 118 | directories := []string{ 119 | p.directory, 120 | path.Join(p.directory, "config"), 121 | path.Join(p.directory, "data"), 122 | path.Join(p.directory, "logs"), 123 | path.Join(p.directory, "msp"), 124 | path.Join(p.directory, "tls"), 125 | } 126 | for _, dir := range directories { 127 | err := os.MkdirAll(dir, 0755) 128 | if err != nil { 129 | return err 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | func (p *Peer) createConfig(dataDirectory, mspDirectory string) error { 136 | fabricConfigPath, ok := os.LookupEnv("FABRIC_CFG_PATH") 137 | if !ok { 138 | return fmt.Errorf("FABRIC_CFG_PATH not defined") 139 | } 140 | configFile := path.Join(fabricConfigPath, "core.yaml") 141 | configData, err := ioutil.ReadFile(configFile) 142 | if err != nil { 143 | return err 144 | } 145 | config := map[interface{}]interface{}{} 146 | err = yaml.Unmarshal(configData, config) 147 | if err != nil { 148 | return err 149 | } 150 | peer, ok := config["peer"].(map[interface{}]interface{}) 151 | if !ok { 152 | return fmt.Errorf("core.yaml missing peer section") 153 | } 154 | peer["id"] = fmt.Sprintf("%speer", strings.ToLower(p.organization.Name())) 155 | peer["mspConfigPath"] = mspDirectory 156 | peer["localMspId"] = p.mspID 157 | peer["fileSystemPath"] = dataDirectory 158 | peer["address"] = fmt.Sprintf("0.0.0.0:%d", p.apiPort) 159 | peer["listenAddress"] = fmt.Sprintf("0.0.0.0:%d", p.apiPort) 160 | peer["chaincodeListenAddress"] = fmt.Sprintf("127.0.0.1:%d", p.chaincodePort) 161 | gossip, ok := peer["gossip"].(map[interface{}]interface{}) 162 | if !ok { 163 | return fmt.Errorf("core.yaml missing peer.gossip section") 164 | } 165 | 166 | gossip["bootstrap"] = p.APIHost(true) 167 | gossip["useLeaderElection"] = false 168 | gossip["orgLeader"] = true 169 | gossip["endpoint"] = p.APIHost(true) 170 | gossip["externalEndpoint"] = p.APIHost(true) 171 | logger.Printf("Creating peer with gossip URL %s", gossip["bootstrap"]) 172 | 173 | metrics, ok := config["metrics"].(map[interface{}]interface{}) 174 | if !ok { 175 | return fmt.Errorf("core.yaml missing metrics section") 176 | } 177 | metrics["provider"] = "prometheus" 178 | operations, ok := config["operations"].(map[interface{}]interface{}) 179 | if !ok { 180 | return fmt.Errorf("core.yaml missing operations section") 181 | } 182 | operations["listenAddress"] = fmt.Sprintf("0.0.0.0:%d", p.operationsPort) 183 | vm, ok := config["vm"].(map[interface{}]interface{}) 184 | if !ok { 185 | return fmt.Errorf("core.yaml missing vm section") 186 | } 187 | vm["endpoint"] = "" 188 | chaincode, ok := config["chaincode"].(map[interface{}]interface{}) 189 | if !ok { 190 | return fmt.Errorf("core.yaml missing chaincode section") 191 | } 192 | homeDirectory, err := util.GetHomeDirectory() 193 | if err != nil { 194 | return err 195 | } 196 | chaincode["externalBuilders"] = []map[interface{}]interface{}{ 197 | { 198 | "path": path.Join(homeDirectory, "builders", "golang"), 199 | "name": "golang", 200 | "propagateEnvironment": []string{ 201 | "GOCACHE", 202 | "GOENV", 203 | "GOROOT", 204 | "HOME", 205 | }, 206 | }, 207 | { 208 | "path": path.Join(homeDirectory, "builders", "java"), 209 | "name": "java", 210 | "propagateEnvironment": []string{ 211 | "HOME", 212 | "JAVA_HOME", 213 | "MAVEN_OPTS", 214 | }, 215 | }, 216 | { 217 | "path": path.Join(homeDirectory, "builders", "node"), 218 | "name": "node", 219 | "propagateEnvironment": []string{ 220 | "HOME", 221 | "npm_config_cache", 222 | }, 223 | }, 224 | { 225 | "path": path.Join(homeDirectory, "builders", "ccaas"), 226 | "name": "chaincode-as-a-service-builder", 227 | "propagateEnvironment": []string{ 228 | "CHAINCODE_AS_A_SERVICE_BUILDER_CONFIG", 229 | }, 230 | }, 231 | { 232 | "path": path.Join(homeDirectory, "builders", "external"), 233 | "name": "external-service-builder", 234 | "propagateEnvironment": []string{ 235 | "HOME", 236 | }, 237 | }, 238 | } 239 | ledger, ok := config["ledger"].(map[interface{}]interface{}) 240 | if !ok { 241 | return fmt.Errorf("core.yaml missing ledger section") 242 | } 243 | state, ok := ledger["state"].(map[interface{}]interface{}) 244 | if !ok { 245 | return fmt.Errorf("core.yaml missing ledger.state section") 246 | } 247 | 248 | snapshots, ok := ledger["snapshots"].(map[interface{}]interface{}) 249 | if !ok { 250 | return fmt.Errorf("core.yaml missing ledger.snapshots section") 251 | } 252 | snapshots["rootDir"] = path.Join(dataDirectory, "snapshots") 253 | 254 | if p.couchDB { 255 | state["stateDatabase"] = "CouchDB" 256 | couchDBConfig, ok := state["couchDBConfig"].(map[interface{}]interface{}) 257 | if !ok { 258 | return fmt.Errorf("core.yaml missing ledger.state.couchDBConfig section") 259 | } 260 | couchDBConfig["couchDBAddress"] = fmt.Sprintf("localhost:%d", p.couchDBPort) 261 | couchDBConfig["username"] = "admin" 262 | couchDBConfig["password"] = "adminpw" 263 | 264 | } 265 | if p.tls != nil { 266 | tlsDirectory := path.Join(p.directory, "tls") 267 | certFile := path.Join(tlsDirectory, "cert.pem") 268 | keyFile := path.Join(tlsDirectory, "key.pem") 269 | caFile := path.Join(tlsDirectory, "ca.pem") 270 | tls, ok := peer["tls"].(map[interface{}]interface{}) 271 | if !ok { 272 | return fmt.Errorf("core.yaml missing peer.tls section") 273 | } 274 | tls["enabled"] = true 275 | tls["cert"] = map[string]string{"file": certFile} 276 | tls["key"] = map[string]string{"file": keyFile} 277 | 278 | tls["rootcert"] = map[string]string{"file": caFile} 279 | tls["clientRootCAs"] = map[string]string{"file": caFile} 280 | tls, ok = operations["tls"].(map[interface{}]interface{}) 281 | if !ok { 282 | return fmt.Errorf("core.yaml missing operations.tls section") 283 | } 284 | tls["enabled"] = true 285 | tls["cert"] = map[string]string{"file": certFile} 286 | tls["key"] = map[string]string{"file": keyFile} 287 | 288 | if err := ioutil.WriteFile(certFile, p.tls.Certificate().Bytes(), 0644); err != nil { 289 | return err 290 | } 291 | if err := ioutil.WriteFile(keyFile, p.tls.PrivateKey().Bytes(), 0644); err != nil { 292 | return err 293 | } 294 | if err := ioutil.WriteFile(caFile, p.tls.CA().Bytes(), 0644); err != nil { 295 | return err 296 | } 297 | } 298 | configData, err = yaml.Marshal(config) 299 | if err != nil { 300 | return err 301 | } 302 | configFile = path.Join(p.directory, "config", "core.yaml") 303 | return ioutil.WriteFile(configFile, configData, 0644) 304 | } 305 | 306 | func (p *Peer) hasStarted() bool { 307 | cli := &http.Client{ 308 | Transport: &http.Transport{ 309 | TLSClientConfig: &tls.Config{ 310 | InsecureSkipVerify: true, 311 | }, 312 | }, 313 | } 314 | resp, err := cli.Get(fmt.Sprintf("%s/healthz", p.OperationsURL(true))) 315 | if err != nil { 316 | log.Printf("error waiting for peer: %v\n", err) 317 | return false 318 | } 319 | if resp.StatusCode != 200 { 320 | return false 321 | } 322 | connection, err := Connect(p, p.mspID, p.organization.Admin()) 323 | if err != nil { 324 | return false 325 | } 326 | defer connection.Close() 327 | _, err = connection.ListChannels() 328 | if err != nil { 329 | return false 330 | } 331 | return true 332 | } 333 | --------------------------------------------------------------------------------