├── docs ├── images │ └── tape.jpeg ├── roadmap.md ├── zh │ ├── workflow.md │ └── gettingstarted.md ├── workflow.md └── gettingstarted.md ├── test ├── andLogic.rego ├── orLogic.rego ├── config14org1andorg2.yaml ├── config20selectendorser.yaml ├── config20org1andorg2.yaml └── configlatest.yaml ├── .dockerignore ├── pkg └── infra │ ├── infra_suite_test.go │ ├── basic │ ├── basic_suite_test.go │ ├── crypto.go │ ├── client_test.go │ ├── client.go │ ├── config_test.go │ ├── logger.go │ └── config.go │ ├── bitmap │ ├── bitmap_suite_test.go │ ├── bitmap.go │ └── bitmap_test.go │ ├── observer │ ├── observer_suite_test.go │ ├── endorsementObersver.go │ ├── endorsementObersver_test.go │ ├── observer.go │ ├── benchmark_test.go │ ├── observerFactory.go │ ├── commitObserver.go │ ├── block_collector.go │ └── observer_test.go │ ├── trafficGenerator │ ├── trafficGenerator_suite_test.go │ ├── policyHandler.go │ ├── fuzzing_test.go │ ├── intgerator.go │ ├── initiator.go │ ├── assembler.go │ ├── fackEnvelopGenerator.go │ ├── benchmark_test.go │ ├── proposal_test.go │ ├── broadcaster.go │ ├── generatorFactory.go │ ├── policyHandler_test.go │ ├── proposer.go │ ├── proposer_test.go │ ├── fackEnvelopGenerator_test.go │ └── initiator_test.go │ ├── cmdImpl │ ├── version.go │ ├── processTemplate.go │ └── fullProcess.go │ └── interface.go ├── MAINTAINERS.md ├── Dockerfile ├── .github ├── dependabot.yml ├── workflows │ ├── golangci-lint.yml │ ├── depbot-auto.yml │ ├── latestImage.yml │ ├── no_func_test.yml │ ├── test.yml │ ├── release.yml │ └── scorecard.yml ├── stale.yml └── ISSUE_TEMPLATE │ ├── new_feature.md │ └── bug_report.md ├── .gitignore ├── e2e ├── e2e_version_cmd_test.go ├── endorsementOnly_test.go ├── commitOnly_test.go ├── e2e_suite_test.go ├── mock │ ├── server.go │ ├── peer.go │ └── orderer.go ├── TrafficAndObserver_test.go ├── util.go └── multi_peer_test.go ├── gotools.mk ├── internal └── fabric │ ├── core │ └── comm │ │ ├── util.go │ │ ├── creds.go │ │ ├── config.go │ │ └── client.go │ ├── common │ └── crypto │ │ └── random.go │ ├── bccsp │ └── utils │ │ ├── keys.go │ │ └── ecdsa.go │ └── protoutil │ ├── unmarshalers.go │ └── commonutils.go ├── scripts ├── functions.sh └── golinter.sh ├── .golangci.yml ├── CONTRIBUTING.md ├── config.yaml ├── README-zh.md ├── Makefile ├── logo.svg ├── README.md ├── go.mod └── cmd └── tape └── main.go /docs/images/tape.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperledger-TWGC/tape/HEAD/docs/images/tape.jpeg -------------------------------------------------------------------------------- /test/andLogic.rego: -------------------------------------------------------------------------------- 1 | package tape 2 | 3 | default allow = false 4 | 5 | allow { 6 | input[_] == "org1" 7 | input[_] == "org2" 8 | } -------------------------------------------------------------------------------- /test/orLogic.rego: -------------------------------------------------------------------------------- 1 | package tape 2 | 3 | default allow = false 4 | allow { 5 | input[_] == "org1" 6 | } 7 | 8 | allow { 9 | input[_] == "org2" 10 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .idea 3 | .vscode 4 | .gitignore 5 | .dockerignore 6 | .git 7 | .DS_Store 8 | 9 | fabric-samples 10 | vendor 11 | 12 | test 13 | config.yaml 14 | organizations 15 | -------------------------------------------------------------------------------- /pkg/infra/infra_suite_test.go: -------------------------------------------------------------------------------- 1 | package infra_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestInfra(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Infra Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/infra/basic/basic_suite_test.go: -------------------------------------------------------------------------------- 1 | package basic_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBasic(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Basic Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/infra/bitmap/bitmap_suite_test.go: -------------------------------------------------------------------------------- 1 | package bitmap_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBitmap(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Bitmap Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/infra/observer/observer_suite_test.go: -------------------------------------------------------------------------------- 1 | package observer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestObserver(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Observer Suite") 13 | } 14 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 维护者信息 2 | 3 | | 姓名 | 邮箱 | github-ID | 4 | | ------ | ------------------------ | ----------- | 5 | | 郭剑南 Jay Gou | guojiannan1101@gmail.com | guoger | 6 | | 袁怿 Sam Yuan | yy19902439@126.com | SamYuan1990 | 7 | | 程阳 Stone Cheng | chengyang418@163.com | stone-ch | 8 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/trafficGenerator_suite_test.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTrafficGenerator(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "TrafficGenerator Suite") 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu as tape-base 2 | 3 | FROM golang:1.24 as golang 4 | 5 | WORKDIR /root 6 | 7 | #ENV GOPROXY=https://goproxy.cn,direct 8 | ENV export GOSUMDB=off 9 | 10 | COPY . . 11 | 12 | RUN go build -v ./cmd/tape 13 | 14 | FROM tape-base 15 | RUN mkdir -p /config 16 | COPY --from=golang /root/tape /usr/local/bin 17 | EXPOSE 8080 18 | 19 | CMD ["tape"] 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | groups: 8 | go-dependencies: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: github-actions 12 | directory: / 13 | schedule: 14 | interval: daily 15 | groups: 16 | github-actions: 17 | patterns: 18 | - "*" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | ./tape 8 | ./tape_pqc 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Files from IDE 17 | *.idea/ 18 | *.vscode/ 19 | 20 | e2e/Tape.log 21 | # local dependency 22 | vendor 23 | 24 | tape 25 | Tape.log 26 | 27 | .cache -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | pull_request: 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | golangci: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: actions/setup-go@v6 15 | with: 16 | go-version-file: go.mod 17 | - run: go mod download 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v9 20 | with: 21 | version: v2.1 22 | skip-pkg-cache: true -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # roadmap 2 | Ref to https://www.hyperledger.org/learn/publications/blockchain-performance-metrics 3 | 4 | ## 0.2.x 5 | - Support traffic with constant rate for durability test (#45) 6 | - Support Test peer/orderer **separately** / support query (#56) 7 | - ~~Report Time when response received – submit time latency for tx (#94)~~ 8 | - Support input arguments templating (#109) 9 | 10 | ## 0.3.x 11 | - Report Time when response received – submit time latency for tx (#94) 12 | - Supports success rate and further enhancment for tx recording(#14) 13 | - SDK adaptor -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/policyHandler.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 7 | "github.com/open-policy-agent/opa/rego" 8 | ) 9 | 10 | func CheckPolicy(input *basic.Elements, rule string) (bool, error) { 11 | if input.Processed { 12 | return false, nil 13 | } 14 | rego := rego.New( 15 | rego.Query("data.tape.allow"), 16 | rego.Module("", rule), 17 | rego.Input(input.Orgs), 18 | ) 19 | rs, err := rego.Eval(context.Background()) 20 | if err != nil { 21 | return false, err 22 | } 23 | input.Processed = rs.Allowed() 24 | return rs.Allowed(), nil 25 | } 26 | -------------------------------------------------------------------------------- /docs/zh/workflow.md: -------------------------------------------------------------------------------- 1 | # 工作流程 2 | 3 | Tape 有多个工作协程组成,所以该流程是高度并行化且可扩展的。这些协程通过缓存通道互相连接,所以它们可以互相传递数据。 4 | 整体工作流程如下图: 5 | 6 | ![tape workflow](images/tape.jpeg) 7 | 8 | - **Signer**,签名交易提案协程,负责签名生成的交易提案,并将签名后的结果存入缓存通道中; 9 | - **Proposer**,提案发送线程,负责从缓存通道中取出已签名的交易提案,然后通过 gRPC 将已签名提案发送到背书节点,并将背书节点返回的背书结果写入另一个缓存通道; 10 | - **Integrator**,负责从缓存通道中取出背书后的结果,并封装成信封,信封是排序节点可接受的格式,然后将该信封再次存入一个单独的缓存通道; 11 | - **Broadcaster**,负责将从缓存通道中取出信封,并然后通过 gRPC 将信封广播到排序节点; 12 | 13 | 以上四个协程可以启动不止一个,因此 Tape 实现高性能和可扩展性,Tape 自身不会成为性能瓶颈。 14 | 15 | 排序节点生成区块后,会将区块广播到 Peer 节点,Peer 节点接收到区块并经过验证保存到本地账本之后,会向其他节点广播已提交区块, 16 | 17 | - **Observer**,接收到 Peer 节点广播的区块之后,会计算区块中交易数量,以及总耗时,当接收到区块的交易数量和运行 Tape 时输入的参数一致时,结束运行,并根据总耗时计算 TPS。 18 | -------------------------------------------------------------------------------- /pkg/infra/cmdImpl/version.go: -------------------------------------------------------------------------------- 1 | package cmdImpl 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | const ( 9 | programName = "tape" 10 | ) 11 | 12 | var ( 13 | Version string = "latest" 14 | CommitSHA string = "development build" 15 | BuiltTime string = "Mon Dec 21 19:00:00 2020" 16 | ) 17 | 18 | // GetVersionInfo return version information 19 | // TODO add commit hash, Built info 20 | func GetVersionInfo() string { 21 | return fmt.Sprintf( 22 | "%s:\n Version: %s\n Go version: %s\n Git commit: %s\n Built: %s\n OS/Arch: %s\n", 23 | programName, 24 | Version, 25 | runtime.Version(), 26 | CommitSHA, 27 | BuiltTime, 28 | fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /e2e/e2e_version_cmd_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os/exec" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | . "github.com/onsi/gomega/gbytes" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | var _ = Describe("Mock test for version", func() { 13 | 14 | Context("E2E with correct subcommand", func() { 15 | When("Version subcommand", func() { 16 | It("should return version info", func() { 17 | var err error 18 | cmd := exec.Command(tapeBin, "version") 19 | tapeSession, err = gexec.Start(cmd, nil, nil) 20 | Expect(err).NotTo(HaveOccurred()) 21 | Eventually(tapeSession.Out).Should(Say("tape:\n Version:.*\n Go version:.*\n Git commit:.*\n Built:.*\n OS/Arch:.*\n")) 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /docs/workflow.md: -------------------------------------------------------------------------------- 1 | # workflow 2 | 3 | Tape consists of several workers that run in goroutines, so that the pipeline is highly concurrent and scalable. Workers are connected via buffered channels, so they can pass products around. 4 | 5 | workflow diagram as: 6 | 7 | ![tape workflow](images/tape.jpeg) 8 | 9 | - **Signer**,create and sign a tx, prepare sending to peer for endorsement; 10 | - **Proposer**,sending tx proposal to peer, sending it to peer for endorsemnt, and once recevied peer's response put response into a channel(a Golang element). 11 | - **Integrator**,collection for all endorsement responses from peers, making envelope. 12 | - **Broadcaster**,get envelop and sending to orderer. 13 | - **Observer**,receive peer events once block been committed, and calculate tps etc... -------------------------------------------------------------------------------- /.github/workflows/depbot-auto.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 20 | run: gh pr merge --rebase --auto "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/fuzzing_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package trafficGenerator_test 5 | 6 | import ( 7 | "testing" 8 | "unicode/utf8" 9 | 10 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 11 | ) 12 | 13 | func FuzzConvertString(f *testing.F) { 14 | testcases := []string{"data", "randomString1", "uuid", "randomNumber1_9", "{\"k1\":\"uuid\",\"key2\":\"randomNumber10000_20000\",\"keys\":\"randomString10\"}"} 15 | for _, tc := range testcases { 16 | f.Add(tc) 17 | } 18 | f.Fuzz(func(t *testing.T, orig string) { 19 | data, err := trafficGenerator.ConvertString(orig) 20 | if utf8.ValidString(orig) && err != nil && !utf8.ValidString(data) && len(data) != 0 { 21 | t.Error(err.Error() + " " + orig + " " + data) 22 | } 23 | if !utf8.ValidString(data) { 24 | t.Error("fail to convert utf8 string" + data) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /gotools.mk: -------------------------------------------------------------------------------- 1 | # Copyright IBM Corp All Rights Reserved. 2 | # Copyright London Stock Exchange Group All Rights Reserved. 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | GOTOOLS = goimports golint staticcheck 7 | BUILD_DIR ?= build 8 | GOTOOLS_BINDIR ?= $(shell go env GOBIN) 9 | 10 | # go tool->path mapping 11 | go.fqp.goimports := golang.org/x/tools/cmd/goimports 12 | go.fqp.golint := golang.org/x/lint/golint 13 | go.fqp.staticcheck := honnef.co/go/tools/cmd/staticcheck 14 | 15 | .PHONY: gotools-install 16 | gotools-install: $(patsubst %,$(GOTOOLS_BINDIR)/%, $(GOTOOLS)) 17 | 18 | .PHONY: gotools-clean 19 | gotools-clean: 20 | 21 | # Default rule for gotools uses the name->path map for a generic 'go get' style build 22 | gotool.%: 23 | $(eval TOOL = ${subst gotool.,,${@}}) 24 | @echo "Building ${go.fqp.${TOOL}} -> $(TOOL)" 25 | go install ${go.fqp.${TOOL}}@latest 26 | 27 | $(GOTOOLS_BINDIR)/%: 28 | $(eval TOOL = ${subst $(GOTOOLS_BINDIR)/,,${@}}) 29 | @$(MAKE) -f gotools.mk gotool.$(TOOL) 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Feature 3 | about: Request a new feature request 4 | title: '' 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your proposal related to a problem? 11 | 12 | 16 | 17 | (Write your answer here.) 18 | 19 | ### Describe the solution you'd like 20 | 21 | 28 | 29 | (Describe your proposed solution here.) 30 | 31 | ### Describe alternatives you've considered 32 | 33 | 36 | 37 | (Write your answer here.) 38 | 39 | ### Additional context 40 | 41 | 45 | 46 | (Write your answer here.) 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 1. 23 | 2. 24 | 3. 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Logs** 30 | Attach logs from Tape/Peer/Orderer here. **And please format them with markdown!** 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /pkg/infra/interface.go: -------------------------------------------------------------------------------- 1 | package infra 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hyperledger-twgc/tape/internal/fabric/protoutil" 7 | 8 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 9 | ) 10 | 11 | const ( 12 | FULLPROCESS = 6 13 | TRAFFIC = 7 14 | OBSERVER = 0 15 | ENDORSEMENT = 4 16 | COMMIT = 3 17 | 18 | PROPOSALFILTER = 4 19 | COMMITFILTER = 3 20 | QUERYFILTER = 2 21 | ) 22 | 23 | /* 24 | to do for #127 SM crypto 25 | just need to do an impl for this interface and replace 26 | and impl a function for func (c Config) LoadCrypto() (*CryptoImpl, error) { 27 | as generator 28 | */ 29 | type Crypto interface { 30 | protoutil.Signer 31 | NewSignatureHeader() (*common.SignatureHeader, error) 32 | /*Serialize() ([]byte, error) 33 | Sign(message []byte) ([]byte, error)*/ 34 | } 35 | 36 | /* 37 | as Tape major as Producer and Consumer pattern 38 | define an interface here as Worker with start here 39 | as for #56 and #174,in cli imp adjust sequence of P&C impl to control workflow. 40 | */ 41 | type Worker interface { 42 | Start() 43 | } 44 | 45 | type ObserverWorker interface { 46 | Worker 47 | GetTime() time.Time 48 | } 49 | -------------------------------------------------------------------------------- /internal/fabric/core/comm/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package comm 8 | 9 | import ( 10 | "crypto/x509" 11 | "encoding/pem" 12 | ) 13 | 14 | // AddPemToCertPool adds PEM-encoded certs to a cert pool 15 | func AddPemToCertPool(pemCerts []byte, pool *x509.CertPool) error { 16 | certs, _, err := pemToX509Certs(pemCerts) 17 | if err != nil { 18 | return err 19 | } 20 | for _, cert := range certs { 21 | pool.AddCert(cert) 22 | } 23 | return nil 24 | } 25 | 26 | // parse PEM-encoded certs 27 | func pemToX509Certs(pemCerts []byte) ([]*x509.Certificate, []string, error) { 28 | var certs []*x509.Certificate 29 | var subjects []string 30 | 31 | // it's possible that multiple certs are encoded 32 | for len(pemCerts) > 0 { 33 | var block *pem.Block 34 | block, pemCerts = pem.Decode(pemCerts) 35 | if block == nil { 36 | break 37 | } 38 | 39 | cert, err := x509.ParseCertificate(block.Bytes) 40 | if err != nil { 41 | return nil, []string{}, err 42 | } 43 | 44 | certs = append(certs, cert) 45 | subjects = append(subjects, string(cert.RawSubject)) 46 | } 47 | 48 | return certs, subjects, nil 49 | } 50 | -------------------------------------------------------------------------------- /scripts/functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright IBM Corp. All Rights Reserved. 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | function filterExcludedAndGeneratedFiles { 8 | local excluded_files 9 | excluded_files=( 10 | '\.block$' 11 | '^\.build/' 12 | '^build/' 13 | '(^|/)ci\.properties$' 14 | '(^|/)\.git/' 15 | '\.gen\.go$' 16 | '(^|/)go.mod$' 17 | '(^|/)go.sum$' 18 | '(^|/)Gopkg\.lock$' 19 | '\.html$' 20 | '\.json$' 21 | '\.key$' 22 | '(^|/)LICENSE$' 23 | '\.md$' 24 | '\.pb\.go$' 25 | '\.pem$' 26 | '\.png$' 27 | '\.pptx$' 28 | '\.rst$' 29 | '_sk$' 30 | '\.tx$' 31 | '\.txt$' 32 | '^NOTICE$' 33 | '(^|/)testdata\/' 34 | '(^|/)vendor\/' 35 | '(^|/)Pipfile$' 36 | '(^|/)Pipfile\.lock$' 37 | '(^|/)tox\.ini$' 38 | ) 39 | 40 | local filter 41 | filter=$(local IFS='|' ; echo "${excluded_files[*]}") 42 | 43 | read -ra files <<<"$@" 44 | for f in "${files[@]}"; do 45 | file=$(echo "$f" | grep -Ev "$filter" | sort -u) 46 | if [ -n "$file" ]; then 47 | head -n2 "$file" | grep -qE '// Code generated by' || echo "$file" 48 | fi 49 | done 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/latestImage.yml: -------------------------------------------------------------------------------- 1 | name: dailyImageBuild 2 | 3 | on: 4 | schedule: 5 | - cron: "0 7 * * *" # https://crontab.guru/#0_0_*_*_0 6 | 7 | jobs: 8 | build-and-push-image: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v5 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@master 26 | with: 27 | registry: ghcr.io 28 | username: davidkhala 29 | password: ${{ secrets.TWGC_DAVIDKHALA }} 30 | 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@master 34 | with: 35 | images: ghcr.io/hyperledger-twgc/tape 36 | tags: | 37 | type=ref,event=tag 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@v6 40 | with: 41 | context: . 42 | platforms: linux/amd64,linux/arm64,linux/s390x 43 | push: true 44 | tags: ghcr.io/hyperledger-twgc/tape 45 | labels: latest 46 | -------------------------------------------------------------------------------- /internal/fabric/common/crypto/random.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. 2016 All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package crypto 18 | 19 | import ( 20 | "crypto/rand" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | const ( 26 | // NonceSize is the default NonceSize 27 | NonceSize = 24 28 | ) 29 | 30 | // GetRandomBytes returns len random looking bytes 31 | func GetRandomBytes(len int) ([]byte, error) { 32 | key := make([]byte, len) 33 | 34 | // TODO: rand could fill less bytes then len 35 | _, err := rand.Read(key) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "error getting random bytes") 38 | } 39 | 40 | return key, nil 41 | } 42 | 43 | // GetRandomNonce returns a random byte array of length NonceSize 44 | func GetRandomNonce() ([]byte, error) { 45 | return GetRandomBytes(NonceSize) 46 | } 47 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | #- depguard 7 | #- dupl 8 | #- errcheck 9 | #- goconst 10 | #- gocritic 11 | - gocyclo 12 | - goprintffuncname 13 | - govet 14 | - ineffassign 15 | - misspell 16 | - nakedret 17 | - noctx 18 | - nolintlint 19 | #- staticcheck 20 | #- stylecheck 21 | - unconvert 22 | - unparam 23 | - whitespace 24 | - unused 25 | - asciicheck 26 | 27 | # don't enable: 28 | # - lll 29 | # - dogsled 30 | # - gochecknoinits 31 | # - gomnd 32 | # - unused 33 | 34 | # TODO: try to enable 35 | # - scopelint 36 | # - gochecknoglobals 37 | # - gocognit 38 | # - godot 39 | # - godox 40 | # - goerr113 41 | # - interfacer 42 | # - maligned 43 | # - nestif 44 | # - prealloc 45 | # - testpackage 46 | # - revive 47 | # - wsl 48 | # - funlen 49 | settings: 50 | dupl: 51 | threshold: 100 52 | govet: 53 | settings: 54 | printf: 55 | funcs: 56 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 57 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 58 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 59 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 60 | run: 61 | timeout: 5m 62 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/intgerator.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra" 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Integrator struct { 13 | Signer infra.Crypto 14 | Ctx context.Context 15 | Processed chan *basic.Elements 16 | Envs chan *basic.TracingEnvelope 17 | ErrorCh chan error 18 | Logger *log.Logger 19 | } 20 | 21 | func (integrator *Integrator) assemble(e *basic.Elements) (*basic.TracingEnvelope, error) { 22 | tapeSpan := basic.GetGlobalSpan() 23 | span := tapeSpan.MakeSpan(e.TxId, "", basic.SIGN_ENVELOP, e.Span) 24 | defer span.Finish() 25 | env, err := CreateSignedTx(e.SignedProp, integrator.Signer, e.Responses) 26 | // end integration proposal 27 | basic.LogEvent(integrator.Logger, e.TxId, "CreateSignedEnvelope") 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &basic.TracingEnvelope{Env: env, TxId: e.TxId, Span: e.Span}, nil 32 | } 33 | 34 | func (integrator *Integrator) Start() { 35 | for { 36 | select { 37 | case p := <-integrator.Processed: 38 | e, err := integrator.assemble(p) 39 | if err != nil { 40 | integrator.ErrorCh <- err 41 | return 42 | } 43 | integrator.Envs <- e 44 | case <-integrator.Ctx.Done(): 45 | return 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/infra/bitmap/bitmap.go: -------------------------------------------------------------------------------- 1 | package bitmap 2 | 3 | import "github.com/pkg/errors" 4 | 5 | type BitMap struct { 6 | count int // number of bits set 7 | capability int // total number of bits 8 | bits []uint64 9 | } 10 | 11 | // Has determine whether the specified position is set 12 | func (b *BitMap) Has(num int) bool { 13 | if num >= b.capability { 14 | return false 15 | } 16 | c, bit := num/64, uint(num%64) 17 | return (c < len(b.bits)) && (b.bits[c]&(1< 0 { 54 | bitsLen++ 55 | } 56 | 57 | return BitMap{bits: make([]uint64, bitsLen), capability: cap}, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/initiator.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra" 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | "golang.org/x/time/rate" 12 | ) 13 | 14 | type Initiator struct { 15 | Num int 16 | Burst int 17 | R float64 18 | Config basic.Config 19 | Crypto infra.Crypto 20 | Logger *log.Logger 21 | Raw chan *basic.TracingProposal 22 | ErrorCh chan error 23 | } 24 | 25 | func (initiator *Initiator) Start() { 26 | limit := rate.Inf 27 | ctx := context.Background() 28 | if initiator.R > 0 { 29 | limit = rate.Limit(initiator.R) 30 | } 31 | limiter := rate.NewLimiter(limit, initiator.Burst) 32 | i := 0 33 | for { 34 | if initiator.Num > 0 { 35 | if i == initiator.Num { 36 | return 37 | } 38 | i++ 39 | } 40 | prop, err := CreateProposal( 41 | initiator.Crypto, 42 | initiator.Logger, 43 | initiator.Config.Channel, 44 | initiator.Config.Chaincode, 45 | initiator.Config.Version, 46 | initiator.Config.Args..., 47 | ) 48 | if err != nil { 49 | initiator.ErrorCh <- errors.Wrapf(err, "error creating proposal") 50 | return 51 | } 52 | 53 | if err = limiter.Wait(ctx); err != nil { 54 | initiator.ErrorCh <- errors.Wrapf(err, "error creating proposal") 55 | return 56 | } 57 | initiator.Raw <- prop 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/infra/observer/endorsementObersver.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type EndorseObserver struct { 14 | Envs chan *basic.TracingEnvelope 15 | n int 16 | logger *log.Logger 17 | Now time.Time 18 | once *sync.Once 19 | finishCh chan struct{} 20 | } 21 | 22 | func CreateEndorseObserver(Envs chan *basic.TracingEnvelope, N int, finishCh chan struct{}, once *sync.Once, logger *log.Logger) *EndorseObserver { 23 | return &EndorseObserver{ 24 | Envs: Envs, 25 | n: N, 26 | logger: logger, 27 | finishCh: finishCh, 28 | once: once, 29 | } 30 | } 31 | 32 | func (o *EndorseObserver) Start() { 33 | o.Now = time.Now() 34 | o.logger.Debugf("start observer for endorsement") 35 | i := 0 36 | for { 37 | e := <-o.Envs 38 | tapeSpan := basic.GetGlobalSpan() 39 | tapeSpan.FinishWithMap(e.TxId, "", basic.TRANSACTIONSTART) 40 | i++ 41 | fmt.Printf("Time %8.2fs\tTx %6d Processed\n", time.Since(o.Now).Seconds(), i) 42 | if o.n > 0 { 43 | if o.n == i { 44 | // consider with multiple threads need close this channel, need a once here to avoid channel been closed in multiple times 45 | o.once.Do(func() { 46 | close(o.finishCh) 47 | }) 48 | return 49 | } 50 | } 51 | } 52 | } 53 | 54 | func (o *EndorseObserver) GetTime() time.Time { 55 | return o.Now 56 | } 57 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/assembler.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 7 | 8 | "github.com/hyperledger-twgc/tape/pkg/infra" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type Assembler struct { 14 | Signer infra.Crypto 15 | Ctx context.Context 16 | Raw chan *basic.TracingProposal 17 | Signed []chan *basic.Elements 18 | ErrorCh chan error 19 | Logger *log.Logger 20 | } 21 | 22 | func (a *Assembler) sign(p *basic.TracingProposal) (*basic.Elements, error) { 23 | tapeSpan := basic.GetGlobalSpan() 24 | span := tapeSpan.MakeSpan(p.TxId, "", basic.SIGN_PROPOSAL, p.Span) 25 | defer span.Finish() 26 | 27 | sprop, err := SignProposal(p.Proposal, a.Signer) 28 | if err != nil { 29 | return nil, err 30 | } 31 | basic.LogEvent(a.Logger, p.TxId, "SignProposal") 32 | EndorsementSpan := tapeSpan.MakeSpan(p.TxId, "", basic.ENDORSEMENT, p.Span) 33 | orgs := make([]string, 0) 34 | basic.GetLatencyMap().StartTracing(p.TxId) 35 | return &basic.Elements{TxId: p.TxId, SignedProp: sprop, Span: p.Span, EndorsementSpan: EndorsementSpan, Orgs: orgs}, nil 36 | } 37 | 38 | func (a *Assembler) Start() { 39 | for { 40 | select { 41 | case r := <-a.Raw: 42 | t, err := a.sign(r) 43 | if err != nil { 44 | a.ErrorCh <- err 45 | return 46 | } 47 | for _, v := range a.Signed { 48 | v <- t 49 | } 50 | case <-a.Ctx.Done(): 51 | return 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /e2e/endorsementOnly_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/e2e/mock" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | . "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var _ = Describe("Mock test for good path", func() { 17 | 18 | Context("E2E with multi mocked Fabric", func() { 19 | When("endorsement only", func() { 20 | It("should work properly", func() { 21 | server, err := mock.NewServer(2, nil) 22 | Expect(err).NotTo(HaveOccurred()) 23 | server.Start() 24 | defer server.Stop() 25 | 26 | config, err := os.CreateTemp("", "endorsement-only-config-*.yaml") 27 | Expect(err).NotTo(HaveOccurred()) 28 | paddrs, oaddr := server.Addresses() 29 | configValue := e2e.Values{ 30 | PrivSk: mtlsKeyFile.Name(), 31 | SignCert: mtlsCertFile.Name(), 32 | Mtls: false, 33 | PeersAddrs: paddrs, 34 | OrdererAddr: oaddr, 35 | CommitThreshold: 1, 36 | PolicyFile: PolicyFile.Name(), 37 | } 38 | e2e.GenerateConfigFile(config.Name(), configValue) 39 | 40 | cmd := exec.Command(tapeBin, "endorsementOnly", "-c", config.Name(), "-n", "500") 41 | tapeSession, err = gexec.Start(cmd, nil, nil) 42 | Expect(err).NotTo(HaveOccurred()) 43 | Eventually(tapeSession.Out).Should(Say("Time.*Tx.*")) 44 | }) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /e2e/commitOnly_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/e2e/mock" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | . "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var _ = Describe("Mock test for good path", func() { 17 | 18 | Context("E2E with multi mocked Fabric", func() { 19 | When("envelope only", func() { 20 | PIt("should work properly", func() { 21 | server, err := mock.NewServer(2, nil) 22 | Expect(err).NotTo(HaveOccurred()) 23 | server.Start() 24 | defer server.Stop() 25 | 26 | config, err := os.CreateTemp("", "envelop-only-config-*.yaml") 27 | Expect(err).NotTo(HaveOccurred()) 28 | paddrs, oaddr := server.Addresses() 29 | configValue := e2e.Values{ 30 | PrivSk: mtlsKeyFile.Name(), 31 | SignCert: mtlsCertFile.Name(), 32 | Mtls: false, 33 | PeersAddrs: paddrs, 34 | OrdererAddr: oaddr, 35 | CommitThreshold: 1, 36 | } 37 | e2e.GenerateConfigFile(config.Name(), configValue) 38 | 39 | cmd := exec.Command(tapeBin, "commitOnly", "-c", config.Name(), "-n", "500") 40 | tapeSession, err = gexec.Start(cmd, nil, nil) 41 | Expect(err).NotTo(HaveOccurred()) 42 | Eventually(tapeSession.Err).Should(Say("Time.*Block.*Tx.*")) 43 | 44 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*")) 45 | }) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/config14org1andorg2.yaml: -------------------------------------------------------------------------------- 1 | # Definition of nodes 2 | peer1: &peer1 3 | addr: localhost:7051 4 | org: org1 5 | tls_ca_cert: /config/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem 6 | 7 | peer2: &peer2 8 | addr: localhost:9051 9 | org: org2 10 | tls_ca_cert: /config/crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/tlscacerts/tlsca.org2.example.com-cert.pem 11 | 12 | orderer1: &orderer1 13 | addr: localhost:7050 14 | org: org1 15 | tls_ca_cert: /config/crypto-config/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem 16 | 17 | policyFile: /config/test/andLogic.rego 18 | 19 | # Nodes to interact with 20 | endorsers: 21 | - *peer1 22 | - *peer2 23 | # we might support multi-committer in the future for more complex test scenario, 24 | # i.e. consider tx committed only if it's done on >50% of nodes. But for now, 25 | # it seems sufficient to support single committer. 26 | committers: 27 | - *peer2 28 | 29 | commitThreshold: 1 30 | 31 | orderer: *orderer1 32 | 33 | # Invocation configs 34 | channel: mychannel 35 | chaincode: mycc 36 | args: 37 | - query 38 | - a 39 | mspid: Org1MSP 40 | private_key: /config/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk 41 | sign_cert: /config/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem 42 | num_of_conn: 10 43 | client_per_conn: 10 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 如何贡献 2 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 3 | ## Issue 4 | 5 | 如果你希望为 Tape 提交新的特性或者遇到了任何 Bug,欢迎在 github 仓库中开启新的 [issue](https://github.com/Hyperledger-TWGC/tape/issues),同时也欢迎提交 [pull request](https://github.com/Hyperledger-TWGC/tape/pulls)。 6 | 7 | If you wish for new features or encounter any bug, please feel free to open [issue](https://github.com/Hyperledger-TWGC/tape/issues), and we always welcome [pull request](https://github.com/Hyperledger-TWGC/tape/pulls). 8 | 9 | 10 | 如果你想报告 issue,请通过如下方式打开 debug 日志,并将日志粘贴到 issue 中。 11 | 12 | ``` 13 | export TAPE_LOGLEVEL=debug 14 | ``` 15 | 16 | If you are reporting an issue, please generously turn on debug log with `export TAPE_LOGLEVEL=debug` and paste log in the issue 17 | ## Pull request 18 | 1. Fork the Project 19 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 20 | 3. Commit your Changes (`git commit -s`) 21 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 22 | 5. Open a Pull Request 23 | 24 | ## Others 25 | 如果您希望贡献文档翻译,或者学习教程,也欢迎和[维护者](MAINTAINERS.md)联系。 26 | 27 | Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. -------------------------------------------------------------------------------- /.github/workflows/no_func_test.yml: -------------------------------------------------------------------------------- 1 | name: Github workflow test 2 | on: 3 | pull_request: 4 | jobs: 5 | escapes-test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v5 9 | - uses: actions/setup-go@v6 10 | with: 11 | go-version-file: go.mod 12 | - run: make escapes 13 | name: run escapes check for tape 14 | vuls-test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: actions/setup-go@v6 19 | with: 20 | go-version-file: go.mod 21 | - run: make vuls 22 | name: run vulnerability check for tape 23 | docker-build-test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v5 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | - name: Build 33 | uses: docker/build-push-action@v6 34 | with: 35 | context: . 36 | platforms: linux/amd64,linux/arm64,linux/s390x 37 | tags: ghcr.io/hyperledger-twgc/tape:test 38 | binary-build-test: 39 | strategy: 40 | matrix: 41 | os: [ubuntu-latest, macos-latest, windows-latest] 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v5 45 | - uses: actions/setup-go@v6 46 | with: 47 | go-version-file: go.mod 48 | - run: GOOS=$(go env GOOS) GOARCH=$(go env GOARCH) make tape 49 | shell: bash -------------------------------------------------------------------------------- /e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gexec" 11 | ) 12 | 13 | var ( 14 | mtlsCertFile, mtlsKeyFile, PolicyFile *os.File 15 | tmpDir, tapeBin string 16 | tapeSession *gexec.Session 17 | ) 18 | 19 | func TestE2e(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "E2e Suite") 22 | } 23 | 24 | var _ = BeforeSuite(func() { 25 | var err error 26 | tmpDir, err = os.MkdirTemp("", "tape-e2e-") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | mtlsCertFile, err = os.CreateTemp(tmpDir, "mtls-*.crt") 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | mtlsKeyFile, err = os.CreateTemp(tmpDir, "mtls-*.key") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = e2e.GenerateCertAndKeys(mtlsKeyFile, mtlsCertFile) 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | PolicyFile, err = os.CreateTemp(tmpDir, "policy") 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | err = e2e.GeneratePolicy(PolicyFile) 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | mtlsCertFile.Close() 45 | mtlsKeyFile.Close() 46 | PolicyFile.Close() 47 | 48 | tapeBin, err = gexec.Build("../cmd/tape") 49 | Expect(err).NotTo(HaveOccurred()) 50 | }) 51 | 52 | var _ = AfterEach(func() { 53 | if tapeSession != nil && tapeSession.ExitCode() == -1 { 54 | tapeSession.Kill() 55 | } 56 | }) 57 | 58 | var _ = AfterSuite(func() { 59 | os.RemoveAll(tmpDir) 60 | os.Remove(tapeBin) 61 | }) 62 | -------------------------------------------------------------------------------- /test/config20selectendorser.yaml: -------------------------------------------------------------------------------- 1 | # Definition of nodes 2 | peer1: &peer1 3 | addr: peer0.org1.example.com:7051 4 | org: org1 5 | tls_ca_cert: /config/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem 6 | 7 | peer2: &peer2 8 | addr: peer0.org2.example.com:9051 9 | org: org2 10 | tls_ca_cert: /config/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/tlscacerts/tlsca.org2.example.com-cert.pem 11 | 12 | orderer1: &orderer1 13 | addr: orderer.example.com:7050 14 | org: org1 15 | tls_ca_cert: /config/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem 16 | 17 | policyFile: /config/test/orLogic.rego 18 | 19 | # Nodes to interact with 20 | endorsers: 21 | - *peer1 22 | - *peer2 23 | # we might support multi-committer in the future for more complex test scenario, 24 | # i.e. consider tx committed only if it's done on >50% of nodes. But for now, 25 | # it seems sufficient to support single committer. 26 | committers: 27 | - *peer1 28 | - *peer2 29 | 30 | commitThreshold: 1 31 | 32 | orderer: *orderer1 33 | 34 | # Invocation configs 35 | channel: mychannel 36 | chaincode: basic 37 | args: 38 | - GetAllAssets 39 | mspid: Org1MSP 40 | private_key: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk 41 | sign_cert: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem 42 | num_of_conn: 10 43 | client_per_conn: 10 44 | -------------------------------------------------------------------------------- /test/config20org1andorg2.yaml: -------------------------------------------------------------------------------- 1 | # Definition of nodes 2 | peer1: &peer1 3 | addr: peer0.org1.example.com:7051 4 | org: org1 5 | tls_ca_cert: /config/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem 6 | 7 | peer2: &peer2 8 | addr: peer0.org2.example.com:9051 9 | org: org2 10 | tls_ca_cert: /config/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/tlscacerts/tlsca.org2.example.com-cert.pem 11 | 12 | orderer1: &orderer1 13 | addr: orderer.example.com:7050 14 | org: org1 15 | tls_ca_cert: /config/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem 16 | 17 | policyFile: /config/test/andLogic.rego 18 | 19 | # Nodes to interact with 20 | endorsers: 21 | - *peer1 22 | - *peer2 23 | # we might support multi-committer in the future for more complex test scenario, 24 | # i.e. consider tx committed only if it's done on >50% of nodes. But for now, 25 | # it seems sufficient to support single committer. 26 | committers: 27 | - *peer1 28 | - *peer2 29 | 30 | commitThreshold: 2 31 | 32 | orderer: *orderer1 33 | 34 | # Invocation configs 35 | channel: mychannel 36 | chaincode: basic 37 | args: 38 | - ReadAsset 39 | - asset1 40 | mspid: Org1MSP 41 | private_key: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk 42 | sign_cert: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem 43 | num_of_conn: 10 44 | client_per_conn: 10 45 | -------------------------------------------------------------------------------- /e2e/mock/server.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "google.golang.org/grpc/credentials" 5 | ) 6 | 7 | type Server struct { 8 | peers []*Peer 9 | orderer *Orderer 10 | } 11 | 12 | // this is the channel size for mock server, peer and orderer 13 | // when use or send tx to mock server/peer/orderer 14 | // try not over this size to avoid hang up or over size 15 | const MockTxSize = 1000 16 | 17 | func NewServer(peerN int, credentials credentials.TransportCredentials) (*Server, error) { 18 | var txCs []chan struct{} 19 | var peers []*Peer 20 | 21 | for i := 0; i < peerN; i++ { 22 | txC := make(chan struct{}, MockTxSize) 23 | peer, err := NewPeer(txC, credentials) 24 | if err != nil { 25 | return nil, err 26 | } 27 | peers = append(peers, peer) 28 | txCs = append(txCs, txC) 29 | } 30 | 31 | orderer, err := NewOrderer(txCs, credentials) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &Server{peers: peers, orderer: orderer}, nil 36 | } 37 | 38 | func (s *Server) Start() { 39 | for _, v := range s.peers { 40 | go v.Start() 41 | } 42 | go s.orderer.Start() 43 | } 44 | 45 | func (s *Server) Stop() { 46 | for _, v := range s.peers { 47 | v.Stop() 48 | } 49 | s.orderer.Stop() 50 | } 51 | 52 | func (s *Server) PeersAddresses() (peersAddrs []string) { 53 | peersAddrs = make([]string, len(s.peers)) 54 | for k, v := range s.peers { 55 | peersAddrs[k] = v.Addrs() 56 | } 57 | return 58 | } 59 | 60 | func (s *Server) OrderAddr() string { 61 | return s.orderer.Addrs() 62 | } 63 | 64 | func (s *Server) Addresses() ([]string, string) { 65 | return s.PeersAddresses(), s.OrderAddr() 66 | } 67 | -------------------------------------------------------------------------------- /docs/gettingstarted.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ## Tutorial Video 4 | 5 | [here](https://www.bilibili.com/video/BV1k5411L79A/) is a video demo。 6 | 7 | ## Install 8 | 9 | You are able to install `tape` via ways blow: 10 | 11 | 1. **download binary**:from[release page](https://github.com/hyperledger-twgc/tape/releases)download binary。 12 | 13 | 2. **local complie**: 14 | ```shell 15 | make tape 16 | ``` 17 | 18 | 3. **Docker image**: 19 | ```shell 20 | docker pull ghcr.io/hyperledger-twgc/tape 21 | ``` 22 | 23 | ## Local Docker complie(optional) 24 | ```shell 25 | make docker 26 | ``` 27 | 28 | ## Edit configuration file according to your fabric network 29 | 30 | according to [this](configfile.md) edit config file. 31 | 32 | ## Run 33 | ### default run: 34 | ```shell 35 | ./tape --config=config.yaml --number=40000 36 | ``` 37 | ### CommitOnly 38 | ```shell 39 | docker run -v $PWD:/tmp ghcr.io/hyperledger-twgc/tape tape commitOnly -c $CONFIG_FILE -n 40000 40 | ``` 41 | ### EndorsementOnly 42 | ```shell 43 | docker run -v $PWD:/tmp ghcr.io/hyperledger-twgc/tape tape endorsementOnly -c $CONFIG_FILE -n 40000 44 | ``` 45 | ### Prometheus 46 | ```shell 47 | ./tape --config=config.yaml --number=40000 --prometheus 48 | ``` 49 | and the Prometheus will listen `:8080/metrics`, the metrics names as `transaction_latency_duration` and `read_latency_duration` for now, they are float based time duration. 50 | 51 | ## Log level 52 | environment `TAPE_LOGLEVEL` for log level 53 | ```shell 54 | export TAPE_LOGLEVEL=debug 55 | ``` 56 | 57 | default is `warn`: 58 | - panic 59 | - fatal 60 | - error 61 | - warn 62 | - warning 63 | - info 64 | - debug 65 | - trace 66 | -------------------------------------------------------------------------------- /internal/fabric/core/comm/creds.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package comm 8 | 9 | import ( 10 | "context" 11 | "crypto/tls" 12 | "net" 13 | 14 | "github.com/pkg/errors" 15 | "google.golang.org/grpc/credentials" 16 | ) 17 | 18 | var ErrServerHandshakeNotImplemented = errors.New("core/comm: server handshakes are not implemented with clientCreds") 19 | 20 | type DynamicClientCredentials struct { 21 | TLSConfig *tls.Config 22 | TLSOptions []TLSOption 23 | } 24 | 25 | func (dtc *DynamicClientCredentials) latestConfig() *tls.Config { 26 | tlsConfigCopy := dtc.TLSConfig.Clone() 27 | for _, tlsOption := range dtc.TLSOptions { 28 | tlsOption(tlsConfigCopy) 29 | } 30 | return tlsConfigCopy 31 | } 32 | 33 | func (dtc *DynamicClientCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { 34 | return credentials.NewTLS(dtc.latestConfig()).ClientHandshake(ctx, authority, rawConn) 35 | } 36 | 37 | func (dtc *DynamicClientCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { 38 | return nil, nil, ErrServerHandshakeNotImplemented 39 | } 40 | 41 | func (dtc *DynamicClientCredentials) Info() credentials.ProtocolInfo { 42 | return credentials.NewTLS(dtc.latestConfig()).Info() 43 | } 44 | 45 | func (dtc *DynamicClientCredentials) Clone() credentials.TransportCredentials { 46 | return credentials.NewTLS(dtc.latestConfig()) 47 | } 48 | 49 | func (dtc *DynamicClientCredentials) OverrideServerName(name string) error { 50 | dtc.TLSConfig.ServerName = name 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/infra/basic/crypto.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "crypto/x509" 8 | "encoding/asn1" 9 | "math/big" 10 | 11 | "github.com/hyperledger-twgc/tape/internal/fabric/bccsp/utils" 12 | "github.com/hyperledger-twgc/tape/internal/fabric/common/crypto" 13 | 14 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 15 | ) 16 | 17 | type CryptoConfig struct { 18 | MSPID string 19 | PrivKey string 20 | SignCert string 21 | TLSCACerts []string 22 | } 23 | 24 | type ECDSASignature struct { 25 | R, S *big.Int 26 | } 27 | 28 | type CryptoImpl struct { 29 | Creator []byte 30 | PrivKey *ecdsa.PrivateKey 31 | SignCert *x509.Certificate 32 | } 33 | 34 | func (s *CryptoImpl) Sign(msg []byte) ([]byte, error) { 35 | ri, si, err := ecdsa.Sign(rand.Reader, s.PrivKey, digest(msg)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | si, _, err = utils.ToLowS(&s.PrivKey.PublicKey, si) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return asn1.Marshal(ECDSASignature{ri, si}) 46 | } 47 | 48 | func (s *CryptoImpl) Serialize() ([]byte, error) { 49 | return s.Creator, nil 50 | } 51 | 52 | func (s *CryptoImpl) NewSignatureHeader() (*common.SignatureHeader, error) { 53 | creator, err := s.Serialize() 54 | if err != nil { 55 | return nil, err 56 | } 57 | nonce, err := crypto.GetRandomNonce() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return &common.SignatureHeader{ 63 | Creator: creator, 64 | Nonce: nonce, 65 | }, nil 66 | } 67 | 68 | func digest(in []byte) []byte { 69 | h := sha256.New() 70 | h.Write(in) 71 | return h.Sum(nil) 72 | } 73 | -------------------------------------------------------------------------------- /test/configlatest.yaml: -------------------------------------------------------------------------------- 1 | # Definition of nodes 2 | peer1: &peer1 3 | addr: peer0.org1.example.com:7051 4 | org: org1 5 | tls_ca_cert: /config/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem 6 | 7 | peer2: &peer2 8 | addr: peer0.org2.example.com:9051 9 | org: org2 10 | tls_ca_cert: /config/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/tlscacerts/tlsca.org2.example.com-cert.pem 11 | 12 | orderer1: &orderer1 13 | addr: orderer.example.com:7050 14 | org: org1 15 | tls_ca_cert: /config/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem 16 | 17 | policyFile: /config/test/andLogic.rego 18 | 19 | # Nodes to interact with 20 | endorsers: 21 | - *peer1 22 | - *peer2 23 | # we might support multi-committer in the future for more complex test scenario, 24 | # i.e. consider tx committed only if it's done on >50% of nodes. But for now, 25 | # it seems sufficient to support single committer. 26 | committers: 27 | - *peer1 28 | - *peer2 29 | 30 | commitThreshold: 2 31 | 32 | orderer: *orderer1 33 | 34 | # Invocation configs 35 | channel: mychannel 36 | chaincode: basic 37 | args: 38 | - CreateAsset 39 | - uuid 40 | - randomString8 41 | - randomNumber0_50 42 | - randomString8 43 | - randomNumber0_50 44 | mspid: Org1MSP 45 | private_key: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk 46 | sign_cert: /config/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem 47 | num_of_conn: 10 48 | client_per_conn: 10 49 | -------------------------------------------------------------------------------- /scripts/golinter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Greg Haskins All Rights Reserved. 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | set -e 8 | 9 | # shellcheck source=/dev/null 10 | source "$(cd "$(dirname "$0")" && pwd)/functions.sh" 11 | 12 | fabric_dir="$(cd "$(dirname "$0")/.." && pwd)" 13 | source_dirs=() 14 | while IFS=$'\n' read -r source_dir; do 15 | source_dirs+=("$source_dir") 16 | done < <(go list -f '{{.Dir}}' ./... | sed s,"${fabric_dir}".,,g | cut -f 1 -d / | sort -u) 17 | 18 | echo "Checking with gofmt" 19 | OUTPUT="$(gofmt -l -s "${source_dirs[@]}")" 20 | OUTPUT="$(filterExcludedAndGeneratedFiles "$OUTPUT")" 21 | if [ -n "$OUTPUT" ]; then 22 | echo "The following files contain gofmt errors" 23 | echo "$OUTPUT" 24 | echo "The gofmt command 'gofmt -l -s -w' must be run for these files" 25 | exit 1 26 | fi 27 | 28 | echo "Checking with goimports" 29 | OUTPUT="$(goimports -l "${source_dirs[@]}")" 30 | OUTPUT="$(filterExcludedAndGeneratedFiles "$OUTPUT")" 31 | if [ -n "$OUTPUT" ]; then 32 | echo "The following files contain goimports errors" 33 | echo "$OUTPUT" 34 | echo "The goimports command 'goimports -l -w' must be run for these files" 35 | exit 1 36 | fi 37 | 38 | echo "Checking with go vet" 39 | PRINTFUNCS="Debug,Debugf,Print,Printf,Info,Infof,Warning,Warningf,Error,Errorf,Critical,Criticalf,Sprint,Sprintf,Log,Logf,Panic,Panicf,Fatal,Fatalf,Notice,Noticef,Wrap,Wrapf,WithMessage" 40 | OUTPUT="$(go vet -all -printfuncs "$PRINTFUNCS" ./...)" 41 | if [ -n "$OUTPUT" ]; then 42 | echo "The following files contain go vet errors" 43 | echo "$OUTPUT" 44 | #exit 1 45 | fi 46 | 47 | exit 0 48 | 49 | #echo "Checking with staticcheck" 50 | #OUTPUT="$(staticcheck ./... || true)" 51 | #if [ -n "$OUTPUT" ]; then 52 | # echo "The following staticcheck issues were flagged" 53 | # echo "$OUTPUT" 54 | # exit 1 55 | #fi 56 | -------------------------------------------------------------------------------- /pkg/infra/observer/endorsementObersver_test.go: -------------------------------------------------------------------------------- 1 | package observer_test 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 7 | "github.com/hyperledger-twgc/tape/pkg/infra/observer" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var _ = Describe("EndorsementObersver", func() { 15 | 16 | BeforeEach(func() { 17 | log.New() 18 | 19 | }) 20 | 21 | It("Should work with number limit", func() { 22 | envs := make(chan *basic.TracingEnvelope, 1024) 23 | finishCh := make(chan struct{}) 24 | logger := log.New() 25 | var once sync.Once 26 | instance := observer.CreateEndorseObserver(envs, 2, finishCh, &once, logger) 27 | 28 | go instance.Start() 29 | 30 | envs <- &basic.TracingEnvelope{} 31 | Consistently(finishCh).ShouldNot(BeClosed()) 32 | envs <- &basic.TracingEnvelope{} 33 | Eventually(finishCh).Should(BeClosed()) 34 | }) 35 | 36 | It("Should work with number limit", func() { 37 | envs := make(chan *basic.TracingEnvelope, 1024) 38 | finishCh := make(chan struct{}) 39 | logger := log.New() 40 | var once sync.Once 41 | instance := observer.CreateEndorseObserver(envs, 1, finishCh, &once, logger) 42 | 43 | go instance.Start() 44 | 45 | envs <- &basic.TracingEnvelope{} 46 | Eventually(finishCh).Should(BeClosed()) 47 | }) 48 | 49 | It("Should work without number limit", func() { 50 | envs := make(chan *basic.TracingEnvelope, 1024) 51 | finishCh := make(chan struct{}) 52 | logger := log.New() 53 | var once sync.Once 54 | instance := observer.CreateEndorseObserver(envs, 0, finishCh, &once, logger) 55 | 56 | go instance.Start() 57 | 58 | envs <- &basic.TracingEnvelope{} 59 | Consistently(finishCh).ShouldNot(BeClosed()) 60 | envs <- &basic.TracingEnvelope{} 61 | Eventually(finishCh).ShouldNot(BeClosed()) 62 | }) 63 | 64 | }) 65 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/fackEnvelopGenerator.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "github.com/hyperledger-twgc/tape/internal/fabric/protoutil" 5 | "github.com/hyperledger-twgc/tape/pkg/infra" 6 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 10 | ) 11 | 12 | type FackEnvelopGenerator struct { 13 | Num int 14 | Burst int 15 | R float64 16 | Config basic.Config 17 | Crypto infra.Crypto 18 | Envs chan *basic.TracingEnvelope 19 | ErrorCh chan error 20 | } 21 | 22 | var nonce = []byte("nonce-abc-12345") 23 | var data = []byte("data") 24 | 25 | func (initiator *FackEnvelopGenerator) Start() { 26 | i := 0 27 | for { 28 | if initiator.Num > 0 { 29 | if i == initiator.Num { 30 | return 31 | } 32 | i++ 33 | } 34 | creator, _ := initiator.Crypto.Serialize() 35 | txid := protoutil.ComputeTxID(nonce, creator) 36 | payloadBytes, _ := protoutil.GetBytesPayload(&common.Payload{ 37 | Header: &common.Header{ 38 | ChannelHeader: protoutil.MarshalOrPanic(&common.ChannelHeader{ 39 | Type: int32(common.HeaderType_ENDORSER_TRANSACTION), 40 | ChannelId: initiator.Config.Channel, 41 | TxId: txid, 42 | Epoch: uint64(0), 43 | }), 44 | SignatureHeader: protoutil.MarshalOrPanic(&common.SignatureHeader{ 45 | Creator: creator, 46 | Nonce: nonce, 47 | }), 48 | }, 49 | Data: data, 50 | }) 51 | 52 | signature, _ := initiator.Crypto.Sign(payloadBytes) 53 | 54 | env := &common.Envelope{ 55 | Payload: payloadBytes, 56 | Signature: signature, 57 | } 58 | span := opentracing.GlobalTracer().StartSpan("integrator for endorsements ", opentracing.Tag{Key: "txid", Value: txid}) 59 | initiator.Envs <- &basic.TracingEnvelope{Env: env, TxId: txid, Span: span} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Definition of nodes 2 | # addr address for node 3 | # tls_ca_cert tls cert 4 | peer1: &peer1 5 | addr: localhost:7051 6 | tls_ca_cert: ./organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem 7 | 8 | peer2: &peer2 9 | addr: localhost:9051 10 | tls_ca_cert: ./organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/tlscacerts/tlsca.org2.example.com-cert.pem 11 | 12 | orderer1: &orderer1 13 | addr: localhost:7050 14 | tls_ca_cert: ./organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem 15 | 16 | # Peer Nodes to interact with as endorsement Peers 17 | endorsers: 18 | - *peer1 19 | - *peer2 20 | 21 | # Peer Nodes to interact with as Commit Peers as listening 22 | committers: 23 | - *peer1 24 | - *peer2 25 | # we might support multi-committer in the future for more complex test scenario. 26 | # i.e. consider tx committed only if it's done on >50% of nodes. 27 | # Give your commit Threshold as numbers for peers here. 28 | commitThreshold: 2 29 | 30 | # orderer Nodes to interact with 31 | orderer: *orderer1 32 | 33 | # Invocation configs 34 | channel: mychannel 35 | chaincode: basic 36 | # chain code args below, in a list of str 37 | # we provides 3 kinds of randmon 38 | # uuid 39 | # randomString$length 40 | # randomNumber$min_$max 41 | args: 42 | - CreateAsset 43 | - uuid 44 | - randomString8 45 | - randomNumber0_50 46 | - randomString8 47 | - randomNumber0_50 48 | # Tx submiter information 49 | mspid: Org1MSP 50 | private_key: ./organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk 51 | sign_cert: ./organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem 52 | # network traffic control 53 | num_of_conn: 10 54 | client_per_conn: 10 55 | -------------------------------------------------------------------------------- /internal/fabric/bccsp/utils/keys.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | "crypto/ecdsa" 11 | "crypto/x509" 12 | "encoding/pem" 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | // DERToPrivateKey unmarshals a der to private key 18 | func DERToPrivateKey(der []byte) (key interface{}, err error) { 19 | if key, err = x509.ParsePKCS1PrivateKey(der); err == nil { 20 | return key, nil 21 | } 22 | 23 | if key, err = x509.ParsePKCS8PrivateKey(der); err == nil { 24 | switch key.(type) { 25 | case *ecdsa.PrivateKey: 26 | return 27 | default: 28 | return nil, errors.New("Found unknown private key type in PKCS#8 wrapping") 29 | } 30 | } 31 | 32 | if key, err = x509.ParseECPrivateKey(der); err == nil { 33 | return 34 | } 35 | 36 | return nil, errors.New("Invalid key type. The DER must contain an ecdsa.PrivateKey") 37 | } 38 | 39 | // PEMtoPrivateKey unmarshals a pem to private key 40 | func PEMtoPrivateKey(raw []byte, pwd []byte) (interface{}, error) { 41 | if len(raw) == 0 { 42 | return nil, errors.New("Invalid PEM. It must be different from nil.") 43 | } 44 | block, _ := pem.Decode(raw) 45 | if block == nil { 46 | return nil, fmt.Errorf("Failed decoding PEM. Block must be different from nil. [% x]", raw) 47 | } 48 | 49 | // TODO: derive from header the type of the key 50 | 51 | if x509.IsEncryptedPEMBlock(block) { 52 | if len(pwd) == 0 { 53 | return nil, errors.New("Encrypted Key. Need a password") 54 | } 55 | 56 | decrypted, err := x509.DecryptPEMBlock(block, pwd) 57 | if err != nil { 58 | return nil, fmt.Errorf("Failed PEM decryption [%s]", err) 59 | } 60 | 61 | key, err := DERToPrivateKey(decrypted) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return key, err 66 | } 67 | 68 | cert, err := DERToPrivateKey(block.Bytes) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return cert, err 73 | } 74 | -------------------------------------------------------------------------------- /pkg/infra/basic/client_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package basic_test 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hyperledger-twgc/tape/e2e/mock" 10 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var _ = Describe("Client", func() { 18 | Context("connect with mock peer", func() { 19 | var mockserver *mock.Server 20 | var peerNode, OrdererNode basic.Node 21 | logger := log.New() 22 | 23 | BeforeEach(func() { 24 | mockserver, _ = mock.NewServer(1, nil) 25 | peerNode = basic.Node{ 26 | Addr: mockserver.PeersAddresses()[0], 27 | } 28 | OrdererNode = basic.Node{ 29 | Addr: mockserver.OrderAddr(), 30 | } 31 | mockserver.Start() 32 | }) 33 | 34 | AfterEach(func() { 35 | mockserver.Stop() 36 | }) 37 | 38 | It("connect with mock endorsers", func() { 39 | _, err := basic.CreateEndorserClient(peerNode, logger) 40 | Expect(err).ShouldNot(HaveOccurred()) 41 | }) 42 | 43 | It("connect with mock broadcasters", func() { 44 | _, err := basic.CreateBroadcastClient(context.Background(), peerNode, logger) 45 | Expect(err).ShouldNot(HaveOccurred()) 46 | }) 47 | 48 | It("connect with mock DeliverFilter", func() { 49 | _, err := basic.CreateDeliverFilteredClient(context.Background(), OrdererNode, logger) 50 | Expect(err).ShouldNot(HaveOccurred()) 51 | }) 52 | 53 | It("connect with mock CreateDeliverClient", func() { 54 | _, err := basic.CreateDeliverClient(OrdererNode) 55 | Expect(err).ShouldNot(HaveOccurred()) 56 | }) 57 | 58 | It("wrong addr test", func() { 59 | dummy := basic.Node{ 60 | Addr: "invalid_addr", 61 | TLSCACertByte: []byte(""), 62 | TLSCAKey: "123", 63 | TLSCARoot: "234", 64 | TLSCARootByte: []byte(""), 65 | } 66 | _, err := basic.CreateGRPCClient(dummy) 67 | Expect(err).Should(HaveOccurred()) 68 | }) 69 | }) 70 | 71 | }) 72 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Tape 2 |
3 | 4 |
5 | Tape 是一款轻量级 Hyperledger Fabric 性能测试工具 6 | 7 | [![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/hyperledger-twgc/tape) 8 | [![Github workflow test](https://github.com/Hyperledger-TWGC/tape/actions/workflows/test.yml/badge.svg)](https://github.com/Hyperledger-TWGC/tape/actions/workflows/test.yml) 9 | 10 | ## 项目背景 11 | 12 | Tape 项目原名 Stupid,最初由 超级账本中国技术工作组成员[郭剑南](https://github.com/guoger)开发,目的是提供一款轻量级、可以快速测试 Hyperledger Fabric TPS 值的工具。Stupid 取自[KISS](https://en.wikipedia.org/wiki/KISS_principle) 原则 Keep it Simple and Stupid,目前已正式更名为Tape,字面含义卷尺,寓意测量,测试。 13 | 14 | 目前 Tape 已贡献到超级账本中国技术社区,由[TWGC 性能优化小组](https://github.com/Hyperledger-TWGC/fabric-performance-wiki)负责维护。 15 | 16 | ## 项目特点 17 | 18 | 1. **轻量级**, Tape 实现过程中没有使用 SDK,直接使用 gRPC 向 Fabric 节点发送和接收请求; 19 | 2. **易操作**,通过简单的配置文件和命令即可快速启动测试; 20 | 3. **结果准确**,Tape 直接使用 gRPC 发送交易,并且对交易和区块处理的不同阶段单独拆分,使用协程及通道缓存的方式并行处理,大幅度提升了 Tape 自身的处理效率,从而可以准确的测试出 Fabric 的真实性能。 21 | 4. **参考标准** 其设计和功能参考[性能测试白皮书](https://github.com/Hyperledger-TWGC/fabric-performance-wiki/blob/master/performance-whitepaper.md)。 22 | 23 | Tape由负载生成器客户端和观察者客户端组成。因此Tape仅可以用来对已经完成部署的Fabric网络进行测试。 24 | - 负载生成器客户端 25 | - 直接使用了GRPC链接到被测网络而不使用任何SDK。因此避免了connection profile的配置, 减少了SDK的其他功能,如服务发现,可能带来的性能损耗。 26 | - 观察者客户端会观察在多个peer节点上的提交,但不会进行资源的实时监控。 27 | 28 | ## 文档索引 29 | 30 | 如果你想快速使用 Tape 测试 TPS,请参考[快速开始](docs/zh/gettingstarted.md); 31 | 32 | 如果你想了解配置文件中各项参数的具体含义,请参考[配置文件说明](docs/zh/configfile.md); 33 | 34 | 如果你想详细了解 Tape 工作流程,请参考[工作流程](docs/zh/workflow.md); 35 | 36 | 如果你在使用过程中遇到了问题请参考[FAQ](https://github.com/Hyperledger-TWGC/tape/wiki/FAQ),如果 FAQ 还不能解决你的问题,请在 github 中提 issue,或者发邮件咨询项目维护者。 37 | 38 | 39 | ## [如何贡献](CONTRIBUTING.md) 40 | 41 | ## [维护者信息](MAINTAINERS.md) 42 | 43 | ## 使用许可 44 | 45 | Tape 遵守 [Apache 2.0 开源许可](LICENSE)。 46 | 47 | ## Credits 48 | Icons made by Good Ware from www.flaticon.com -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Functional Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: # pushing tags is also considered as a push. Exclude this here. 6 | - "*" 7 | schedule: 8 | - cron: "0 0 * * 0" # https://crontab.guru/#0_0_*_*_0 9 | jobs: 10 | unit-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: actions/setup-go@v6 15 | with: 16 | go-version-file: go.mod 17 | - run: make tape 18 | name: build tape binary program 19 | - run: make install 20 | name: install tape to GOBIN 21 | - run: make unit-test 22 | name: run unit test 23 | #integration-test-1412: 24 | # runs-on: ubuntu-latest 25 | # needs: unit-test 26 | # strategy: 27 | # matrix: 28 | # FABRIC_VERSION: [1_4] 29 | # INTERGATION_CASE: [ANDLogic] 30 | # steps: 31 | # - uses: actions/checkout@v5 32 | # - uses: actions/setup-go@v6 33 | # with: 34 | # go-version: 1.22 35 | # - run: go mod vendor 36 | # - run: make integration-test FABRIC_VERSION=${{matrix.FABRIC_VERSION}} INTERGATION_CASE=${{matrix.INTERGATION_CASE}} 37 | integration-test: 38 | runs-on: ubuntu-latest 39 | needs: unit-test 40 | strategy: 41 | matrix: 42 | FABRIC_VERSION: [2_5] 43 | INTERGATION_CASE: [ORLogic, ANDLogic, COMMITONLY, ENDORSEMNTONLY] 44 | steps: 45 | - uses: actions/checkout@v5 46 | - uses: actions/setup-go@v6 47 | with: 48 | go-version-file: go.mod 49 | - run: go mod vendor 50 | - run: make integration-test FABRIC_VERSION=${{matrix.FABRIC_VERSION}} INTERGATION_CASE=${{matrix.INTERGATION_CASE}} 51 | integration-test-30: 52 | runs-on: ubuntu-latest 53 | needs: unit-test 54 | strategy: 55 | matrix: 56 | FABRIC_VERSION: [3_0] 57 | INTERGATION_CASE: [ANDLogic] 58 | steps: 59 | - uses: actions/checkout@v5 60 | - uses: actions/setup-go@v6 61 | with: 62 | go-version-file: go.mod 63 | - run: go mod vendor 64 | - run: make integration-test FABRIC_VERSION=${{matrix.FABRIC_VERSION}} INTERGATION_CASE=${{matrix.INTERGATION_CASE}} -------------------------------------------------------------------------------- /pkg/infra/cmdImpl/processTemplate.go: -------------------------------------------------------------------------------- 1 | package cmdImpl 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/observer" 9 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 10 | 11 | "github.com/opentracing/opentracing-go" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type CmdConfig struct { 16 | FinishCh chan struct{} 17 | ErrorCh chan error 18 | cancel context.CancelFunc 19 | Generator *trafficGenerator.TrafficGenerator 20 | Observerfactory *observer.ObserverFactory 21 | Closer io.Closer 22 | } 23 | 24 | func CreateCmd(configPath string, num int, burst, signerNumber, parallel int, rate float64, logger *log.Logger) (*CmdConfig, error) { 25 | config, err := basic.LoadConfig(configPath) 26 | if err != nil { 27 | return nil, err 28 | } 29 | crypto, err := config.LoadCrypto() 30 | if err != nil { 31 | return nil, err 32 | } 33 | raw := make(chan *basic.TracingProposal, burst) 34 | signed := make([]chan *basic.Elements, len(config.Endorsers)) 35 | processed := make(chan *basic.Elements, burst) 36 | envs := make(chan *basic.TracingEnvelope, burst) 37 | 38 | blockCh := make(chan *observer.AddressedBlock) 39 | 40 | finishCh := make(chan struct{}) 41 | errorCh := make(chan error, burst) 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | 44 | for i := 0; i < len(config.Endorsers); i++ { 45 | signed[i] = make(chan *basic.Elements, burst) 46 | } 47 | 48 | tr, closer := basic.Init("tape") 49 | opentracing.SetGlobalTracer(tr) 50 | 51 | mytrafficGenerator := trafficGenerator.NewTrafficGenerator(ctx, 52 | crypto, 53 | envs, 54 | raw, 55 | processed, 56 | signed, 57 | config, 58 | num, 59 | burst, 60 | signerNumber, 61 | parallel, 62 | rate, 63 | logger, 64 | errorCh) 65 | 66 | Observerfactory := observer.NewObserverFactory( 67 | config, 68 | crypto, 69 | blockCh, 70 | logger, 71 | ctx, 72 | finishCh, 73 | num, 74 | parallel, 75 | envs, 76 | errorCh) 77 | cmd := &CmdConfig{ 78 | finishCh, 79 | errorCh, 80 | cancel, 81 | mytrafficGenerator, 82 | Observerfactory, 83 | closer, 84 | } 85 | return cmd, nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/infra/bitmap/bitmap_test.go: -------------------------------------------------------------------------------- 1 | package bitmap_test 2 | 3 | import ( 4 | "github.com/hyperledger-twgc/tape/pkg/infra/bitmap" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Bitmap", func() { 11 | 12 | Context("New BitsMap", func() { 13 | It("the environment is properly set", func() { 14 | b, err := bitmap.NewBitMap(4) 15 | Expect(err).To(BeNil()) 16 | Expect(b.Cap()).To(Equal(4)) 17 | Expect(b.Count()).To(Equal(0)) 18 | Expect(b.BitsLen()).To(Equal(1)) 19 | 20 | b, err = bitmap.NewBitMap(65) 21 | Expect(err).To(BeNil()) 22 | Expect(b.Cap()).To(Equal(65)) 23 | Expect(b.Count()).To(Equal(0)) 24 | Expect(b.BitsLen()).To(Equal(2)) 25 | }) 26 | 27 | It("should error which cap is less than 1", func() { 28 | _, err := bitmap.NewBitMap(0) 29 | Expect(err).NotTo(BeNil()) 30 | 31 | _, err = bitmap.NewBitMap(-1) 32 | Expect(err).NotTo(BeNil()) 33 | }) 34 | }) 35 | 36 | Context("Operate BitsMap", func() { 37 | It("the len of bits is just one ", func() { 38 | b, err := bitmap.NewBitMap(4) 39 | Expect(err).To(BeNil()) 40 | b.Set(0) 41 | Expect(b.Count()).To(Equal(1)) 42 | b.Set(2) 43 | Expect(b.Count()).To(Equal(2)) 44 | ok := b.Has(0) 45 | Expect(ok).To(BeTrue()) 46 | ok = b.Has(2) 47 | Expect(ok).To(BeTrue()) 48 | ok = b.Has(1) 49 | Expect(ok).To(BeFalse()) 50 | ok = b.Has(4) 51 | Expect(ok).To(BeFalse()) 52 | 53 | b.Set(4) 54 | Expect(b.Count()).To(Equal(2)) 55 | b.Set(2) 56 | Expect(b.Count()).To(Equal(2)) 57 | }) 58 | 59 | It("the len of bits is more than one", func() { 60 | b, err := bitmap.NewBitMap(80) 61 | Expect(err).To(BeNil()) 62 | b.Set(0) 63 | Expect(b.Count()).To(Equal(1)) 64 | b.Set(2) 65 | Expect(b.Count()).To(Equal(2)) 66 | b.Set(70) 67 | Expect(b.Count()).To(Equal(3)) 68 | b.Set(79) 69 | Expect(b.Count()).To(Equal(4)) 70 | ok := b.Has(0) 71 | Expect(ok).To(BeTrue()) 72 | ok = b.Has(2) 73 | Expect(ok).To(BeTrue()) 74 | ok = b.Has(70) 75 | Expect(ok).To(BeTrue()) 76 | ok = b.Has(79) 77 | Expect(ok).To(BeTrue()) 78 | ok = b.Has(1) 79 | Expect(ok).To(BeFalse()) 80 | ok = b.Has(3) 81 | Expect(ok).To(BeFalse()) 82 | ok = b.Has(69) 83 | Expect(ok).To(BeFalse()) 84 | 85 | b.Set(80) 86 | Expect(b.Count()).To(Equal(4)) 87 | b.Set(2) 88 | Expect(b.Count()).To(Equal(4)) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /docs/zh/gettingstarted.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 视频教程 4 | 5 | [这里](https://www.bilibili.com/video/BV1k5411L79A/) 有一个视频教程,欢迎观看。 6 | 7 | ## 安装 8 | 9 | 你可以通过以下三种方式安装 `tape`: 10 | 11 | 1. **下载二进制文件**:从[这里](https://github.com/hyperledger-twgc/tape/releases)下载 tar 包,并解压。 12 | 13 | 2. **本地编译**:克隆本仓库并在根目录运行如下命令进行编译: 14 | 15 | ``` 16 | make tape 17 | ``` 18 | 19 | 注意: 20 | 21 | 1. 推荐 Go 1.18。Go 语言的安装请参考[这里](https://golang.google.cn/doc/install)。 22 | 23 | 2. Tape 项目是一个 go module 工程,因此不用将项目保存到 `GOPATH` 下,任意目录都可执行编译操作。执行编译命令之后,它会自动下载相关依赖,下载依赖可能需要一定时间。编译完成后,会在当前目录生成一个名为 tape 的可执行文件。 24 | 25 | 3. 如果下载依赖速度过慢,推荐配置 goproxy 国内代理,配置方式请参考[Goproxy 中国](https://goproxy.cn/)。 26 | 27 | 3. **拉取 Docker 镜像**: 28 | 29 | ```shell 30 | docker pull ghcr.io/hyperledger-twgc/tape 31 | ``` 32 | 33 | ## 编译 Docker(可选) 34 | 35 | Tape 支持本地编译 Docker 镜像,在项目根目录下执行以下命令即可: 36 | 37 | ```shell 38 | make docker 39 | ``` 40 | 41 | 执行成功之后本地会增加一个 ghcr.io/hyperledger-twgc/tape:latest 的 Docker 镜像。 42 | 43 | ## 修改配置文件 44 | 45 | 请根据[配置文件说明](configfile.md)修改配置文件。 46 | 注意:如果需要修改 hosts 文件,请注意相关映射的修改。 47 | 48 | ## 运行 49 | ### 默认模式 50 | 执行如下命令即可运行测试: 51 | ```shell 52 | ./tape --config=config.yaml --number=40000 53 | ``` 54 | 如果需要使用其他模式,请参考: 55 | ### CommitOnly 56 | ```shell 57 | docker run -v $PWD:/tmp ghcr.io/hyperledger-twgc/tape tape commitOnly -c $CONFIG_FILE -n 40000 58 | ``` 59 | ### EndorsementOnly 60 | ```shell 61 | docker run -v $PWD:/tmp ghcr.io/hyperledger-twgc/tape tape endorsementOnly -c $CONFIG_FILE -n 40000 62 | ``` 63 | 64 | 该命令的含义是,使用 config.yaml 作为配置文件,向 Fabric 网络发送40000条交易进行性能测试。 65 | > 使用 `./tape --help` 可以查看 tape 帮助文档 66 | 67 | 注意:**请把发送交易数量设置为 batchsize (Fabric 中 Peer 节点的配置文件 core.yaml 中的参数,表示区块中包含的交易数量)的整倍数,这样最后一个区块就不会因为超时而出块了。** 例如,如果你的区块中包含交易数设为500,那么发送交易数量就应该设为1000、40000、100000这样的值。 68 | 69 | 70 | ## 日志说明 71 | 72 | 我们使用 [logrus](https://github.com/sirupsen/logrus) 来管理日志,请通过环境变量 `TAPE_LOGLEVEL` 来设置日志级别。例如: 73 | 74 | ```shell 75 | export TAPE_LOGLEVEL=debug 76 | ``` 77 | 78 | 日志级别共有如下八级,默认级别为 `warn`: 79 | - panic 80 | - fatal 81 | - error 82 | - warn 83 | - warning 84 | - info 85 | - debug 86 | - trace 87 | 88 | ## 注意事项 89 | 90 | - 请把 Tape 和 Fabric 部署在接近的位置,或者直接部署在同一台机器上。这样就可以防止因网络带宽问题带来的瓶颈。你可以使用类似 `iftop` 这样的工具来监控网络流量。 91 | - 可以使用类似 `top` 这样的指令查看 CPU 状态。在测试刚开始的时候,你可能会看到 CPU 使用率很高,这是因为 Peer 节点在处理提案。这种现象出现的时间会很短,然后你就会看到区块一个接一个的被提交。 92 | - 修改 Fabric 的出块参数,可能会有不同的测试结果。如果你想测试最佳出块参数,请查看 [Probe](https://github.com/SamYuan1990/Probe) 项目,该项目的目的就是测试 Fabric 的最佳出块参数。 93 | -------------------------------------------------------------------------------- /internal/fabric/bccsp/utils/ecdsa.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | "crypto/ecdsa" 11 | "crypto/elliptic" 12 | "encoding/asn1" 13 | "errors" 14 | "fmt" 15 | "math/big" 16 | ) 17 | 18 | type ECDSASignature struct { 19 | R, S *big.Int 20 | } 21 | 22 | var ( 23 | // curveHalfOrders contains the precomputed curve group orders halved. 24 | // It is used to ensure that signature' S value is lower or equal to the 25 | // curve group order halved. We accept only low-S signatures. 26 | // They are precomputed for efficiency reasons. 27 | curveHalfOrders = map[elliptic.Curve]*big.Int{ 28 | elliptic.P224(): new(big.Int).Rsh(elliptic.P224().Params().N, 1), 29 | elliptic.P256(): new(big.Int).Rsh(elliptic.P256().Params().N, 1), 30 | elliptic.P384(): new(big.Int).Rsh(elliptic.P384().Params().N, 1), 31 | elliptic.P521(): new(big.Int).Rsh(elliptic.P521().Params().N, 1), 32 | } 33 | ) 34 | 35 | func UnmarshalECDSASignature(raw []byte) (*big.Int, *big.Int, error) { 36 | // Unmarshal 37 | sig := new(ECDSASignature) 38 | _, err := asn1.Unmarshal(raw, sig) 39 | if err != nil { 40 | return nil, nil, fmt.Errorf("failed unmashalling signature [%s]", err) 41 | } 42 | 43 | // Validate sig 44 | if sig.R == nil { 45 | return nil, nil, errors.New("invalid signature, R must be different from nil") 46 | } 47 | if sig.S == nil { 48 | return nil, nil, errors.New("invalid signature, S must be different from nil") 49 | } 50 | 51 | if sig.R.Sign() != 1 { 52 | return nil, nil, errors.New("invalid signature, R must be larger than zero") 53 | } 54 | if sig.S.Sign() != 1 { 55 | return nil, nil, errors.New("invalid signature, S must be larger than zero") 56 | } 57 | 58 | return sig.R, sig.S, nil 59 | } 60 | 61 | // IsLow checks that s is a low-S 62 | func IsLowS(k *ecdsa.PublicKey, s *big.Int) (bool, error) { 63 | halfOrder, ok := curveHalfOrders[k.Curve] 64 | if !ok { 65 | return false, fmt.Errorf("curve not recognized [%s]", k.Curve) 66 | } 67 | 68 | return s.Cmp(halfOrder) != 1, nil 69 | } 70 | 71 | func ToLowS(k *ecdsa.PublicKey, s *big.Int) (*big.Int, bool, error) { 72 | lowS, err := IsLowS(k, s) 73 | if err != nil { 74 | return nil, false, err 75 | } 76 | 77 | if !lowS { 78 | // Set s to N - s that will be then in the lower part of signature space 79 | // less or equal to half order 80 | s.Sub(k.Params().N, s) 81 | 82 | return s, true, nil 83 | } 84 | 85 | return s, false, nil 86 | } 87 | -------------------------------------------------------------------------------- /e2e/mock/peer.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials" 10 | ) 11 | 12 | type Peer struct { 13 | Listener net.Listener 14 | GrpcServer *grpc.Server 15 | BlkSize, txCnt uint64 16 | TxC chan struct{} 17 | ctlCh chan bool 18 | } 19 | 20 | func (p *Peer) ProcessProposal(context.Context, *peer.SignedProposal) (*peer.ProposalResponse, error) { 21 | return &peer.ProposalResponse{Response: &peer.Response{Status: 200}}, nil 22 | } 23 | 24 | func (p *Peer) Deliver(peer.Deliver_DeliverServer) error { 25 | panic("Not implemented") 26 | } 27 | 28 | func (p *Peer) DeliverFiltered(srv peer.Deliver_DeliverFilteredServer) error { 29 | _, err := srv.Recv() 30 | if err != nil { 31 | panic("expect no recv error") 32 | } 33 | _ = srv.Send(&peer.DeliverResponse{}) 34 | txc := p.TxC 35 | for { 36 | select { 37 | case <-txc: 38 | p.txCnt++ 39 | if p.txCnt%p.BlkSize == 0 { 40 | _ = srv.Send(&peer.DeliverResponse{Type: &peer.DeliverResponse_FilteredBlock{ 41 | FilteredBlock: &peer.FilteredBlock{ 42 | Number: p.txCnt / p.BlkSize, 43 | FilteredTransactions: make([]*peer.FilteredTransaction, p.BlkSize)}}}) 44 | } 45 | case pause := <-p.ctlCh: 46 | if pause { 47 | txc = nil 48 | } else { 49 | txc = p.TxC 50 | } 51 | } 52 | } 53 | } 54 | 55 | func (p *Peer) DeliverWithPrivateData(peer.Deliver_DeliverWithPrivateDataServer) error { 56 | panic("Not implemented") 57 | } 58 | 59 | func (p *Peer) Stop() { 60 | p.GrpcServer.Stop() 61 | p.Listener.Close() 62 | } 63 | 64 | func (p *Peer) Start() { 65 | _ = p.GrpcServer.Serve(p.Listener) 66 | } 67 | 68 | func (p *Peer) Addrs() string { 69 | return p.Listener.Addr().String() 70 | } 71 | 72 | func NewPeer(TxC chan struct{}, credentials credentials.TransportCredentials) (*Peer, error) { 73 | lis, err := net.Listen("tcp", "127.0.0.1:0") 74 | if err != nil { 75 | return nil, err 76 | } 77 | ctlCh := make(chan bool) 78 | instance := &Peer{ 79 | Listener: lis, 80 | GrpcServer: grpc.NewServer(grpc.Creds(credentials)), 81 | BlkSize: 10, 82 | TxC: TxC, 83 | ctlCh: ctlCh, 84 | } 85 | 86 | peer.RegisterEndorserServer(instance.GrpcServer, instance) 87 | peer.RegisterDeliverServer(instance.GrpcServer, instance) 88 | 89 | return instance, nil 90 | } 91 | 92 | func (p *Peer) Pause() { 93 | p.ctlCh <- true 94 | } 95 | 96 | func (p *Peer) Unpause() { 97 | p.ctlCh <- false 98 | } 99 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/benchmark_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package trafficGenerator_test 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/hyperledger-twgc/tape/e2e" 11 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 12 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 13 | ) 14 | 15 | func benchmarkProposalRandom(b *testing.B, arg string) { 16 | b.ReportAllocs() 17 | b.ResetTimer() 18 | for i := 0; i < b.N; i++ { 19 | _, _ = trafficGenerator.ConvertString(arg) 20 | } 21 | b.StopTimer() 22 | } 23 | 24 | func BenchmarkProposalRandomTest1(b *testing.B) { 25 | benchmarkProposalRandom(b, "data") 26 | } 27 | 28 | func BenchmarkProposalRandomTest2(b *testing.B) { 29 | benchmarkProposalRandom(b, "randomString1") 30 | } 31 | 32 | func BenchmarkProposalRandomTest3(b *testing.B) { 33 | benchmarkProposalRandom(b, "{\"key\":\"randomNumber1_50\",\"key1\":\"randomNumber1_20\"}") 34 | } 35 | 36 | func BenchmarkProposalRandomTest4(b *testing.B) { 37 | benchmarkProposalRandom(b, "{\"k1\":\"uuid\",\"key2\":\"randomNumber10000_20000\",\"keys\":\"randomString10\"}") 38 | } 39 | 40 | func BenchmarkFackEnvelopTest(b *testing.B) { 41 | errorCh := make(chan error, 1000) 42 | envs := make(chan *basic.TracingEnvelope, 1000) 43 | tmpDir, _ := os.MkdirTemp("", "tape-") 44 | mtlsCertFile, _ := os.CreateTemp(tmpDir, "mtls-*.crt") 45 | mtlsKeyFile, _ := os.CreateTemp(tmpDir, "mtls-*.key") 46 | PolicyFile, _ := os.CreateTemp(tmpDir, "policy") 47 | 48 | _ = e2e.GeneratePolicy(PolicyFile) 49 | _ = e2e.GenerateCertAndKeys(mtlsKeyFile, mtlsCertFile) 50 | mtlsCertFile.Close() 51 | mtlsKeyFile.Close() 52 | PolicyFile.Close() 53 | configFile, _ := os.CreateTemp(tmpDir, "config*.yaml") 54 | configValue := e2e.Values{ 55 | PrivSk: mtlsKeyFile.Name(), 56 | SignCert: mtlsCertFile.Name(), 57 | Mtls: false, 58 | PeersAddrs: nil, 59 | OrdererAddr: "", 60 | CommitThreshold: 1, 61 | PolicyFile: PolicyFile.Name(), 62 | } 63 | e2e.GenerateConfigFile(configFile.Name(), configValue) 64 | config, _ := basic.LoadConfig(configFile.Name()) 65 | crypto, _ := config.LoadCrypto() 66 | fackEnvelopGenerator := &trafficGenerator.FackEnvelopGenerator{ 67 | Num: b.N, 68 | Burst: 1000, 69 | R: 0, 70 | Config: config, 71 | Crypto: crypto, 72 | Envs: envs, 73 | ErrorCh: errorCh, 74 | } 75 | b.ReportAllocs() 76 | b.ResetTimer() 77 | go fackEnvelopGenerator.Start() 78 | var n int 79 | for n < b.N { 80 | <-envs 81 | n++ 82 | } 83 | b.StopTimer() 84 | os.RemoveAll(tmpDir) 85 | } 86 | -------------------------------------------------------------------------------- /e2e/TrafficAndObserver_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/e2e/mock" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | . "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var _ = Describe("Mock test for good path", func() { 17 | 18 | Context("E2E with multi mocked Fabric", func() { 19 | When("traffic and observer mode", func() { 20 | 21 | It("should work properly", func() { 22 | server, err := mock.NewServer(2, nil) 23 | Expect(err).NotTo(HaveOccurred()) 24 | server.Start() 25 | defer server.Stop() 26 | 27 | config, err := os.CreateTemp("", "endorsement-only-config-*.yaml") 28 | Expect(err).NotTo(HaveOccurred()) 29 | paddrs, oaddr := server.Addresses() 30 | configValue := e2e.Values{ 31 | PrivSk: mtlsKeyFile.Name(), 32 | SignCert: mtlsCertFile.Name(), 33 | Mtls: false, 34 | PeersAddrs: paddrs, 35 | OrdererAddr: oaddr, 36 | CommitThreshold: 1, 37 | } 38 | e2e.GenerateConfigFile(config.Name(), configValue) 39 | 40 | cmd0 := exec.Command(tapeBin, "traffic", "-c", config.Name(), "--rate=10") 41 | 42 | //cmd1 := exec.Command(tapeBin, "observer", "-c", config.Name()) 43 | tapeSession, err = gexec.Start(cmd0, nil, nil) 44 | Expect(err).NotTo(HaveOccurred()) 45 | //_, err = gexec.Start(cmd0, nil, nil) 46 | //Expect(err).NotTo(HaveOccurred()) 47 | //Eventually(tapeSession.Out).Should(Say("Time.*Tx.*")) 48 | }) 49 | 50 | It("should work properly", func() { 51 | server, err := mock.NewServer(2, nil) 52 | Expect(err).NotTo(HaveOccurred()) 53 | server.Start() 54 | defer server.Stop() 55 | 56 | config, err := os.CreateTemp("", "endorsement-only-config-*.yaml") 57 | Expect(err).NotTo(HaveOccurred()) 58 | paddrs, oaddr := server.Addresses() 59 | configValue := e2e.Values{ 60 | PrivSk: mtlsKeyFile.Name(), 61 | SignCert: mtlsCertFile.Name(), 62 | Mtls: false, 63 | PeersAddrs: paddrs, 64 | OrdererAddr: oaddr, 65 | CommitThreshold: 1, 66 | PolicyFile: PolicyFile.Name(), 67 | } 68 | e2e.GenerateConfigFile(config.Name(), configValue) 69 | 70 | cmd0 := exec.Command(tapeBin, "traffic", "-c", config.Name()) 71 | 72 | cmd1 := exec.Command(tapeBin, "observer", "-c", config.Name()) 73 | tapeSession, err = gexec.Start(cmd1, nil, nil) 74 | Expect(err).NotTo(HaveOccurred()) 75 | _, err = gexec.Start(cmd0, nil, nil) 76 | Expect(err).NotTo(HaveOccurred()) 77 | Eventually(tapeSession.Out).Should(Say("Time.*Tx.*")) 78 | }) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Github workflow test"] 6 | types: [completed] 7 | workflow_dispatch: 8 | push: 9 | tags: 10 | - "v*.*.*" 11 | 12 | jobs: 13 | build-and-push-image: 14 | if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v5 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v3 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Log in to the Container registry 31 | uses: docker/login-action@master 32 | with: 33 | registry: ghcr.io 34 | username: davidkhala 35 | password: ${{ secrets.TWGC_DAVIDKHALA }} 36 | 37 | - name: Extract metadata (tags, labels) for Docker 38 | id: meta 39 | uses: docker/metadata-action@master 40 | with: 41 | images: ghcr.io/hyperledger-twgc/tape 42 | tags: | 43 | type=ref,event=tag 44 | - if: github.ref == 'refs/heads/master' 45 | name: Build and push Docker image 46 | uses: docker/build-push-action@v6 47 | with: 48 | context: . 49 | platforms: linux/amd64,linux/arm64,linux/s390x 50 | push: true 51 | tags: ghcr.io/hyperledger-twgc/tape 52 | labels: ${{ steps.meta.outputs.labels }} 53 | build-and-release-binary: 54 | if: startsWith(github.event.ref, 'refs/tags/') 55 | strategy: 56 | matrix: 57 | os: [ubuntu-latest, macos-latest, windows-latest] 58 | runs-on: ${{ matrix.os }} 59 | steps: 60 | - uses: actions/checkout@v5 61 | - uses: actions/setup-go@v6 62 | with: 63 | go-version-file: go.mod 64 | - run: GOOS=$(go env GOOS) GOARCH=$(go env GOARCH) make tape 65 | shell: bash 66 | - run: cp tape tape-${{runner.os}}-${{runner.arch}} 67 | - name: Release 68 | uses: softprops/action-gh-release@master 69 | with: 70 | files: | 71 | tape-${{runner.os}}-${{runner.arch}} 72 | config.yaml 73 | - name: Generate SBOM 74 | uses: anchore/sbom-action@v0.20.10 75 | with: 76 | path: ./ 77 | artifact-name: tape-${{ steps.meta.outputs.labels }}.json 78 | output-file: ./tape-${{ steps.meta.outputs.labels }}.spdx.json 79 | - name: Attach SBOM to release 80 | uses: softprops/action-gh-release@v2 81 | with: 82 | tag_name: tape-${{ steps.meta.outputs.labels }} 83 | files: ./tape-${{ steps.meta.outputs.labels }}.spdx.json 84 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/proposal_test.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator_test 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Proposal", func() { 14 | Context("ConvertString", func() { 15 | It("work accordingly for string", func() { 16 | input := "data" 17 | data, err := trafficGenerator.ConvertString(input) 18 | Expect(err).NotTo(HaveOccurred()) 19 | Expect(data).To(Equal("data")) 20 | }) 21 | 22 | It("work accordingly for random str", func() { 23 | input := "randomString1" 24 | data, err := trafficGenerator.ConvertString(input) 25 | Expect(err).NotTo(HaveOccurred()) 26 | Expect(len(data)).To(Equal(1)) 27 | }) 28 | 29 | It("work accordingly for uuid", func() { 30 | input := "uuid" 31 | data, err := trafficGenerator.ConvertString(input) 32 | Expect(err).NotTo(HaveOccurred()) 33 | Expect(len(data) > 0).To(BeTrue()) 34 | }) 35 | 36 | It("work accordingly for randomNumber", func() { 37 | input := "randomNumber1_9" 38 | data, err := trafficGenerator.ConvertString(input) 39 | Expect(err).NotTo(HaveOccurred()) 40 | Expect(len(data)).To(Equal(1)) 41 | num, err := strconv.Atoi(data) 42 | Expect(err).NotTo(HaveOccurred()) 43 | Expect(num > 0).To(BeTrue()) 44 | }) 45 | 46 | It("work accordingly for string mix mode", func() { 47 | input := "{\"key\":\"randomNumber1_50\",\"key1\":\"randomNumber1_20\"}" 48 | data, err := trafficGenerator.ConvertString(input) 49 | Expect(err).NotTo(HaveOccurred()) 50 | regString, err := regexp.Compile("{\"key\":\"\\d*\",\"key1\":\"\\d*\"}") 51 | Expect(err).NotTo(HaveOccurred()) 52 | Expect(regString.MatchString(data)).To(BeTrue()) 53 | }) 54 | 55 | It("work accordingly for string mix mode 2", func() { 56 | input := "{\"k1\":\"uuid\",\"key2\":\"randomNumber10000_20000\",\"keys\":\"randomString10\"}" 57 | data, err := trafficGenerator.ConvertString(input) 58 | Expect(err).NotTo(HaveOccurred()) 59 | regString, err := regexp.Compile("{\"k1\":\".*\",\"key2\":\"\\d*\",\"keys\":\".*\"}") 60 | Expect(err).NotTo(HaveOccurred()) 61 | Expect(regString.MatchString(data)).To(BeTrue()) 62 | }) 63 | 64 | It("handle edge case for randmon number", func() { 65 | input := "randomNumber1_00" 66 | _, err := trafficGenerator.ConvertString(input) 67 | Expect(err).To(HaveOccurred()) 68 | }) 69 | 70 | It("handle edge case for randmon number", func() { 71 | input := "randomNumber1_1" 72 | _, err := trafficGenerator.ConvertString(input) 73 | Expect(err).To(HaveOccurred()) 74 | }) 75 | 76 | It("handle edge case for randomstring", func() { 77 | input := "0randomString166666666011010" 78 | _, err := trafficGenerator.ConvertString(input) 79 | Expect(err).To(HaveOccurred()) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # Only the default branch is supported. 9 | branch_protection_rule: 10 | schedule: 11 | # Weekly on Saturdays. 12 | - cron: '30 1 * * 6' 13 | push: 14 | branches: [ main, master ] 15 | 16 | # Declare default permissions as read only. 17 | permissions: read-all 18 | 19 | jobs: 20 | analysis: 21 | name: Scorecard analysis 22 | runs-on: ubuntu-latest 23 | permissions: 24 | # Needed to upload the results to code-scanning dashboard. 25 | security-events: write 26 | # Needed to publish results and get a badge (see publish_results below). 27 | id-token: write 28 | # Uncomment the permissions below if installing in a private repository. 29 | # contents: read 30 | # actions: read 31 | 32 | steps: 33 | - name: "Checkout code" 34 | uses: actions/checkout@v5 35 | with: 36 | persist-credentials: false 37 | 38 | - name: "Run analysis" 39 | uses: ossf/scorecard-action@v2.4.3 40 | with: 41 | results_file: scorecard-results.sarif 42 | results_format: sarif 43 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 44 | # - you want to enable the Branch-Protection check on a *public* repository, or 45 | # - you are installing Scorecard on a *private* repository 46 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 47 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 48 | 49 | # Public repositories: 50 | # - Publish results to OpenSSF REST API for easy access by consumers 51 | # - Allows the repository to include the Scorecard badge. 52 | # - See https://github.com/ossf/scorecard-action#publishing-results. 53 | # For private repositories: 54 | # - `publish_results` will always be set to `false`, regardless 55 | # of the value entered here. 56 | publish_results: true 57 | 58 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 59 | # format to the repository Actions tab. 60 | - name: "Upload artifact" 61 | uses: actions/upload-artifact@v5 62 | with: 63 | name: SARIF file 64 | path: scorecard-results.sarif 65 | retention-days: 5 66 | 67 | # Upload the results to GitHub's code scanning dashboard. 68 | - name: "Upload to code-scanning" 69 | uses: github/codeql-action/upload-sarif@v4 70 | with: 71 | sarif_file: scorecard-results.sarif 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------- 2 | # This makefile defines the following targets 3 | # - tape - builds a binary program 4 | # - docker - build the tape image 5 | # - unit-test - runs the go-test based unit tests 6 | # - integration-test - runs the integration tests 7 | # - install - installs a binary program to GOBIN path 8 | 9 | FABRIC_VERSION = latest 10 | INTERGATION_CASE = ANDLogic 11 | 12 | BASE_VERSION = 0.2.0 13 | PREV_VERSION = 0.1.2 14 | 15 | PROJECT_NAME = tape 16 | DOCKERIMAGE = ghcr.io/hyperledger-twgc/tape 17 | export DOCKERIMAGE 18 | EXTRA_VERSION ?= $(shell git rev-parse --short HEAD) 19 | BuiltTime ?= $(shell date) 20 | PROJECT_VERSION=$(BASE_APISERVER_VERSION)-snapshot-$(EXTRA_VERSION) 21 | 22 | PKGNAME = github.com/hyperledger-twgc/$(PROJECT_NAME) 23 | CGO_FLAGS = CGO_CFLAGS=" " 24 | ARCH=$(shell go env GOARCH) 25 | MARCH=$(shell go env GOOS)-$(shell go env GOARCH) 26 | 27 | # defined in pkg/infra/version.go 28 | METADATA_VAR = Version=$(BASE_VERSION) 29 | METADATA_VAR += CommitSHA=$(EXTRA_VERSION) 30 | 31 | GO_LDFLAGS = $(patsubst %,-X $(PKGNAME)/pkg/infra/cmdImpl.%,$(METADATA_VAR)) 32 | GO_LDFLAGS += -X '$(PKGNAME)/pkg/infra/cmdImpl.BuiltTime=$(BuiltTime)' 33 | 34 | GO_TAGS ?= 35 | 36 | export GO_LDFLAGS GO_TAGS FABRIC_VERSION INTERGATION_CASE 37 | 38 | base_dir := $(patsubst %/,%,$(dir $(realpath $(lastword $(MAKEFILE_LIST))))) 39 | 40 | tape: 41 | @echo "Building tape program......" 42 | go build -tags "$(GO_TAGS)" -ldflags "$(GO_LDFLAGS)" ./cmd/tape 43 | 44 | escapes: 45 | @echo "go build check for escapes" 46 | go build -gcflags="-m -l" ./... | grep "escapes to heap" || true 47 | 48 | set_govulncheck: 49 | @go install golang.org/x/vuln/cmd/govulncheck@latest 50 | 51 | vuls: set_govulncheck 52 | @govulncheck -v ./... || true 53 | 54 | .PHONY: docker 55 | docker: 56 | @echo "Building tape docker......" 57 | docker build . --tag=ghcr.io/hyperledger-twgc/tape 58 | 59 | .PHONY: unit-test 60 | unit-test: 61 | @echo "Run unit test......" 62 | go test -v ./... --race --bench=. -cover --count=1 63 | go test -fuzz=Fuzz -fuzztime 30s ./pkg/infra/trafficGenerator 64 | 65 | .PHONY: integration-test 66 | integration-test: 67 | @echo "Run integration test......" 68 | ./test/integration-test.sh $(FABRIC_VERSION) $(INTERGATION_CASE) 69 | 70 | .PHONY: install 71 | install: 72 | @echo "Install tape......" 73 | go install -tags "$(GO_TAGS)" -ldflags "$(GO_LDFLAGS)" ./cmd/tape 74 | 75 | include gotools.mk 76 | 77 | .PHONY: checks 78 | checks: gotools-install linter 79 | 80 | .PHONY: linter 81 | linter: 82 | go mod vendor 83 | docker pull golangci/golangci-lint:latest 84 | docker run --tty --rm \ 85 | --volume '$(base_dir)/.cache/golangci-lint:/root/.cache' \ 86 | --volume '$(base_dir):/app' \ 87 | --workdir /app \ 88 | golangci/golangci-lint \ 89 | golangci-lint run --verbose 90 | -------------------------------------------------------------------------------- /e2e/mock/orderer.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | 8 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 9 | "github.com/hyperledger/fabric-protos-go-apiv2/orderer" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials" 12 | ) 13 | 14 | type Orderer struct { 15 | Listener net.Listener 16 | GrpcServer *grpc.Server 17 | cnt uint64 18 | TxCs []chan struct{} 19 | SelfC chan struct{} 20 | } 21 | 22 | func (o *Orderer) Deliver(srv orderer.AtomicBroadcast_DeliverServer) error { 23 | _, err := srv.Recv() 24 | if err != nil { 25 | panic("expect no recv error") 26 | } 27 | _ = srv.Send(&orderer.DeliverResponse{}) 28 | for range o.SelfC { 29 | o.cnt++ 30 | if o.cnt%10 == 0 { 31 | _ = srv.Send(&orderer.DeliverResponse{ 32 | Type: &orderer.DeliverResponse_Block{Block: NewBlock(10, nil)}, 33 | }) 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | func (o *Orderer) Broadcast(srv orderer.AtomicBroadcast_BroadcastServer) error { 40 | for { 41 | _, err := srv.Recv() 42 | if err == io.EOF { 43 | return nil 44 | } 45 | 46 | if err != nil { 47 | fmt.Println(err) 48 | return err 49 | } 50 | 51 | for _, c := range o.TxCs { 52 | c <- struct{}{} 53 | } 54 | o.SelfC <- struct{}{} 55 | 56 | err = srv.Send(&orderer.BroadcastResponse{Status: common.Status_SUCCESS}) 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | } 62 | 63 | func NewOrderer(txCs []chan struct{}, credentials credentials.TransportCredentials) (*Orderer, error) { 64 | lis, err := net.Listen("tcp", "127.0.0.1:0") 65 | if err != nil { 66 | return nil, err 67 | } 68 | instance := &Orderer{ 69 | Listener: lis, 70 | GrpcServer: grpc.NewServer(grpc.Creds(credentials)), 71 | TxCs: txCs, 72 | SelfC: make(chan struct{}), 73 | } 74 | orderer.RegisterAtomicBroadcastServer(instance.GrpcServer, instance) 75 | return instance, nil 76 | } 77 | 78 | func (o *Orderer) Stop() { 79 | o.GrpcServer.Stop() 80 | o.Listener.Close() 81 | } 82 | 83 | func (o *Orderer) Addrs() string { 84 | return o.Listener.Addr().String() 85 | } 86 | 87 | func (o *Orderer) Start() { 88 | _ = o.GrpcServer.Serve(o.Listener) 89 | } 90 | 91 | // NewBlock constructs a block with no data and no metadata. 92 | func NewBlock(seqNum uint64, previousHash []byte) *common.Block { 93 | block := &common.Block{} 94 | block.Header = &common.BlockHeader{} 95 | block.Header.Number = seqNum 96 | block.Header.PreviousHash = previousHash 97 | block.Header.DataHash = []byte{} 98 | block.Data = &common.BlockData{} 99 | 100 | var metadataContents [][]byte 101 | for i := 0; i < len(common.BlockMetadataIndex_name); i++ { 102 | metadataContents = append(metadataContents, []byte{}) 103 | } 104 | block.Metadata = &common.BlockMetadata{Metadata: metadataContents} 105 | 106 | return block 107 | } 108 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/broadcaster.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | 10 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 11 | "github.com/hyperledger/fabric-protos-go-apiv2/orderer" 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Broadcasters struct { 17 | workers []*Broadcaster 18 | ctx context.Context 19 | envs chan *basic.TracingEnvelope 20 | errorCh chan error 21 | } 22 | 23 | type Broadcaster struct { 24 | c orderer.AtomicBroadcast_BroadcastClient 25 | logger *log.Logger 26 | } 27 | 28 | func CreateBroadcasters(ctx context.Context, envs chan *basic.TracingEnvelope, errorCh chan error, config basic.Config, logger *log.Logger) (*Broadcasters, error) { 29 | var workers []*Broadcaster 30 | for i := 0; i < config.NumOfConn; i++ { 31 | broadcaster, err := CreateBroadcaster(ctx, config.Orderer, logger) 32 | if err != nil { 33 | return nil, err 34 | } 35 | workers = append(workers, broadcaster) 36 | } 37 | 38 | return &Broadcasters{ 39 | workers: workers, 40 | ctx: ctx, 41 | envs: envs, 42 | errorCh: errorCh, 43 | }, nil 44 | } 45 | 46 | func (bs Broadcasters) Start() { 47 | for _, b := range bs.workers { 48 | go b.StartDraining(bs.errorCh) 49 | go b.Start(bs.ctx, bs.envs, bs.errorCh) 50 | } 51 | } 52 | 53 | func CreateBroadcaster(ctx context.Context, node basic.Node, logger *log.Logger) (*Broadcaster, error) { 54 | client, err := basic.CreateBroadcastClient(ctx, node, logger) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return &Broadcaster{c: client, logger: logger}, nil 60 | } 61 | 62 | func (b *Broadcaster) Start(ctx context.Context, envs <-chan *basic.TracingEnvelope, errorCh chan error) { 63 | b.logger.Debugf("Start sending broadcast") 64 | for { 65 | select { 66 | case e := <-envs: 67 | //b.logger.Debugf("Sending broadcast envelop") 68 | tapeSpan := basic.GetGlobalSpan() 69 | span := tapeSpan.MakeSpan(e.TxId, "", basic.BROADCAST, e.Span) 70 | err := b.c.Send(e.Env) 71 | if err != nil { 72 | errorCh <- err 73 | } 74 | span.Finish() 75 | e.Span.Finish() 76 | if basic.GetMod() == infra.FULLPROCESS { 77 | Global_Span := tapeSpan.GetSpan(e.TxId, "", basic.TRANSACTION) 78 | tapeSpan.SpanIntoMap(e.TxId, "", basic.CONSESUS, Global_Span) 79 | } else { 80 | tapeSpan.SpanIntoMap(e.TxId, "", basic.CONSESUS, nil) 81 | } 82 | 83 | e = nil 84 | // end of transaction 85 | case <-ctx.Done(): 86 | return 87 | } 88 | } 89 | } 90 | 91 | func (b *Broadcaster) StartDraining(errorCh chan error) { 92 | for { 93 | res, err := b.c.Recv() 94 | if err != nil { 95 | if err == io.EOF { 96 | return 97 | } 98 | b.logger.Errorf("recv broadcast err: %+v, status: %+v\n", err, res) 99 | return 100 | } 101 | 102 | if res.Status != common.Status_SUCCESS { 103 | errorCh <- errors.Errorf("recv errouneous status %s", res.Status) 104 | return 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tape 2 |
3 | 4 |
5 | 6 | [![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/hyperledger-twgc/tape) 7 | [![Github workflow test](https://github.com/Hyperledger-TWGC/tape/actions/workflows/test.yml/badge.svg)](https://github.com/Hyperledger-TWGC/tape/actions/workflows/test.yml)[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7388/badge)](https://bestpractices.coreinfrastructure.org/projects/7388)[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Hyperledger-TWGC/tape/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Hyperledger-TWGC/tape) 8 | 9 | Online doc on [zread](https://zread.ai/Hyperledger-TWGC/tape/1-overview) 10 | 11 | A light-weight tool to test performance of Hyperledger Fabric 12 | 13 | It is used to perform super simple performance test. 14 | Our main focus is to make sure that *tape will not be the bottleneck of performance test* 15 | 16 | README in English/[中文](README-zh.md) 17 | 18 | 19 | ## Table Of Content 20 | 21 | - [Tape](#tape) 22 | - [Table Of Content](#table-of-content) 23 | - [Prerequisites](#prerequisites) 24 | - [Configure](#configure) 25 | - [Usage](#usage) 26 | - [Binary](#binary) 27 | - [Docker](#docker) 28 | - [FAQ](#faq) 29 | - [Contribute](#contribute) 30 | - [License](#license) 31 | - [Contact](#contact) 32 | - [Credits](#credits) 33 | - [Contributors](#contributors) 34 | 35 | --- 36 | ## Prerequisites 37 | 38 | You could get `tape` in three ways: 39 | 1. Download binary: get release tar from [release page](https://github.com/hyperledger-twgc/tape/releases), and extract `tape` binary from it 40 | 2. Build from source: clone this repo and run `make tape` at root dir. `tape` binary will be available at project root directory. 41 | 3. Pull docker image: `docker pull ghcr.io/hyperledger-twgc/tape` 42 | --- 43 | 44 | ## [Configure](docs/configfile.md) 45 | 46 | ## Usage 47 | For further usage, please see [getting start](/docs/gettingstarted.md) 48 | 49 | ### Binary 50 | Execute `./tape -c config.yaml -n 40000` to generate 40000 transactions to Fabric. 51 | 52 | 53 | ### Docker 54 | ```shell 55 | docker run -v $PWD:/tmp ghcr.io/hyperledger-twgc/tape tape -c $CONFIG_FILE -n 40000 56 | ``` 57 | 58 | *Set this to integer times of batchsize, so that last block is not cut due to timeout*. For example, if you have batch size of 500, set this to 500, 1000, 40000, 100000, etc. 59 | 60 | ## [FAQ](https://github.com/Hyperledger-TWGC/tape/wiki/FAQ) 61 | 62 | --- 63 | ## Contribute 64 | [How to Contribute](CONTRIBUTING.md) 65 | [workflow](docs/workflow.md) 66 | 67 | --- 68 | ## License 69 | Hyperledger Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the [LICENSE](LICENSE) file. 70 | 71 | --- 72 | ## Contact 73 | 74 | * [Maintainers](MAINTAINERS.md) 75 | --- 76 | 77 | ## Credits 78 | 79 | Icons made by Good Ware from www.flaticon.com 80 | 81 | --- 82 | ## Contributors 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hyperledger-twgc/tape 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gogo/protobuf v1.3.2 7 | github.com/golang/protobuf v1.5.4 8 | github.com/google/uuid v1.6.0 9 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 10 | github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7 11 | github.com/onsi/ginkgo/v2 v2.25.3 12 | github.com/onsi/gomega v1.38.2 13 | github.com/open-policy-agent/opa v0.70.0 14 | github.com/opentracing/opentracing-go v1.2.0 15 | github.com/pkg/errors v0.9.1 16 | github.com/prometheus/client_golang v1.23.2 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/uber/jaeger-client-go v2.30.0+incompatible 19 | golang.org/x/time v0.13.0 20 | google.golang.org/grpc v1.75.1 21 | google.golang.org/protobuf v1.36.9 22 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 23 | gopkg.in/yaml.v2 v2.4.0 24 | ) 25 | 26 | require ( 27 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 28 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 29 | github.com/OneOfOne/xxhash v1.2.8 // indirect 30 | github.com/agnivade/levenshtein v1.2.0 // indirect 31 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 32 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 36 | github.com/go-ini/ini v1.67.0 // indirect 37 | github.com/go-logr/logr v1.4.3 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 40 | github.com/gobwas/glob v0.2.3 // indirect 41 | github.com/google/go-cmp v0.7.0 // indirect 42 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect 43 | github.com/gorilla/mux v1.8.1 // indirect 44 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 45 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 46 | github.com/prometheus/client_model v0.6.2 // indirect 47 | github.com/prometheus/common v0.66.1 // indirect 48 | github.com/prometheus/procfs v0.16.1 // indirect 49 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 50 | github.com/stretchr/objx v0.5.0 // indirect 51 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 52 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 53 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 54 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 55 | github.com/yashtewari/glob-intersection v0.2.0 // indirect 56 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 57 | go.opentelemetry.io/otel v1.37.0 // indirect 58 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 59 | go.opentelemetry.io/otel/sdk v1.37.0 // indirect 60 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 61 | go.uber.org/atomic v1.11.0 // indirect 62 | go.uber.org/automaxprocs v1.6.0 // indirect 63 | go.yaml.in/yaml/v2 v2.4.2 // indirect 64 | go.yaml.in/yaml/v3 v3.0.4 // indirect 65 | golang.org/x/net v0.43.0 // indirect 66 | golang.org/x/sys v0.35.0 // indirect 67 | golang.org/x/text v0.28.0 // indirect 68 | golang.org/x/tools v0.36.0 // indirect 69 | google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 70 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 71 | gopkg.in/yaml.v3 v3.0.1 // indirect 72 | sigs.k8s.io/yaml v1.4.0 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /internal/fabric/protoutil/unmarshalers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package protoutil 8 | 9 | import ( 10 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 11 | cb "github.com/hyperledger/fabric-protos-go-apiv2/common" 12 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 13 | "github.com/pkg/errors" 14 | "google.golang.org/protobuf/proto" 15 | ) 16 | 17 | // UnmarshalPayload unmarshals bytes to a Payload 18 | func UnmarshalPayload(encoded []byte) (*cb.Payload, error) { 19 | payload := &cb.Payload{} 20 | err := proto.Unmarshal(encoded, payload) 21 | return payload, errors.Wrap(err, "error unmarshaling Payload") 22 | } 23 | 24 | // UnmarshalEnvelope unmarshals bytes to a Envelope 25 | func UnmarshalEnvelope(encoded []byte) (*cb.Envelope, error) { 26 | envelope := &cb.Envelope{} 27 | err := proto.Unmarshal(encoded, envelope) 28 | return envelope, errors.Wrap(err, "error unmarshaling Envelope") 29 | } 30 | 31 | // UnmarshalChannelHeader unmarshals bytes to a ChannelHeader 32 | func UnmarshalChannelHeader(bytes []byte) (*cb.ChannelHeader, error) { 33 | chdr := &cb.ChannelHeader{} 34 | err := proto.Unmarshal(bytes, chdr) 35 | return chdr, errors.Wrap(err, "error unmarshaling ChannelHeader") 36 | } 37 | 38 | // UnmarshalSignatureHeader unmarshals bytes to a SignatureHeader 39 | func UnmarshalSignatureHeader(bytes []byte) (*cb.SignatureHeader, error) { 40 | sh := &common.SignatureHeader{} 41 | err := proto.Unmarshal(bytes, sh) 42 | return sh, errors.Wrap(err, "error unmarshaling SignatureHeader") 43 | } 44 | 45 | // UnmarshalHeader unmarshals bytes to a Header 46 | func UnmarshalHeader(bytes []byte) (*common.Header, error) { 47 | hdr := &common.Header{} 48 | err := proto.Unmarshal(bytes, hdr) 49 | return hdr, errors.Wrap(err, "error unmarshaling Header") 50 | } 51 | 52 | // UnmarshalChaincodeAction unmarshals bytes to a ChaincodeAction 53 | func UnmarshalChaincodeAction(caBytes []byte) (*peer.ChaincodeAction, error) { 54 | chaincodeAction := &peer.ChaincodeAction{} 55 | err := proto.Unmarshal(caBytes, chaincodeAction) 56 | return chaincodeAction, errors.Wrap(err, "error unmarshaling ChaincodeAction") 57 | } 58 | 59 | // UnmarshalProposalResponsePayload unmarshals bytes to a ProposalResponsePayload 60 | func UnmarshalProposalResponsePayload(prpBytes []byte) (*peer.ProposalResponsePayload, error) { 61 | prp := &peer.ProposalResponsePayload{} 62 | err := proto.Unmarshal(prpBytes, prp) 63 | return prp, errors.Wrap(err, "error unmarshaling ProposalResponsePayload") 64 | } 65 | 66 | // UnmarshalTransaction unmarshals bytes to a Transaction 67 | func UnmarshalTransaction(txBytes []byte) (*peer.Transaction, error) { 68 | tx := &peer.Transaction{} 69 | err := proto.Unmarshal(txBytes, tx) 70 | return tx, errors.Wrap(err, "error unmarshaling Transaction") 71 | } 72 | 73 | // UnmarshalChaincodeActionPayload unmarshals bytes to a ChaincodeActionPayload 74 | func UnmarshalChaincodeActionPayload(capBytes []byte) (*peer.ChaincodeActionPayload, error) { 75 | cap := &peer.ChaincodeActionPayload{} 76 | err := proto.Unmarshal(capBytes, cap) 77 | return cap, errors.Wrap(err, "error unmarshaling ChaincodeActionPayload") 78 | } 79 | 80 | // UnmarshalChaincodeProposalPayload unmarshals bytes to a ChaincodeProposalPayload 81 | func UnmarshalChaincodeProposalPayload(bytes []byte) (*peer.ChaincodeProposalPayload, error) { 82 | cpp := &peer.ChaincodeProposalPayload{} 83 | err := proto.Unmarshal(bytes, cpp) 84 | return cpp, errors.Wrap(err, "error unmarshaling ChaincodeProposalPayload") 85 | } 86 | -------------------------------------------------------------------------------- /pkg/infra/observer/observer.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 10 | 11 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Observers struct { 17 | workers []*Observer 18 | errorCh chan error 19 | blockCh chan *AddressedBlock 20 | ctx context.Context 21 | } 22 | 23 | type Observer struct { 24 | index int 25 | Address string 26 | d peer.Deliver_DeliverFilteredClient 27 | logger *log.Logger 28 | } 29 | 30 | type key string 31 | 32 | const start key = "start" 33 | 34 | func CreateObservers(ctx context.Context, crypto infra.Crypto, errorCh chan error, blockCh chan *AddressedBlock, config basic.Config, logger *log.Logger) (*Observers, error) { 35 | var workers []*Observer 36 | for i, node := range config.Committers { 37 | worker, err := CreateObserver(ctx, config.Channel, node, crypto, logger) 38 | if err != nil { 39 | return nil, err 40 | } 41 | worker.index = i 42 | workers = append(workers, worker) 43 | } 44 | return &Observers{ 45 | workers: workers, 46 | errorCh: errorCh, 47 | blockCh: blockCh, 48 | ctx: ctx, 49 | }, nil 50 | } 51 | 52 | func (o *Observers) Start() { 53 | //o.StartTime = time.Now() 54 | 55 | o.ctx = context.WithValue(o.ctx, start, time.Now()) 56 | for i := 0; i < len(o.workers); i++ { 57 | go o.workers[i].Start(o.errorCh, o.blockCh, o.ctx.Value(start).(time.Time)) 58 | } 59 | } 60 | 61 | func (o *Observers) GetTime() time.Time { 62 | return o.ctx.Value(start).(time.Time) 63 | } 64 | 65 | func CreateObserver(ctx context.Context, channel string, node basic.Node, crypto infra.Crypto, logger *log.Logger) (*Observer, error) { 66 | seek, err := trafficGenerator.CreateSignedDeliverNewestEnv(channel, crypto) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | deliverer, err := basic.CreateDeliverFilteredClient(ctx, node, logger) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | if err = deliverer.Send(seek); err != nil { 77 | return nil, err 78 | } 79 | 80 | // drain first response 81 | if _, err = deliverer.Recv(); err != nil { 82 | return nil, err 83 | } 84 | 85 | return &Observer{Address: node.Addr, d: deliverer, logger: logger}, nil 86 | } 87 | 88 | func (o *Observer) Start(errorCh chan error, blockCh chan<- *AddressedBlock, now time.Time) { 89 | o.logger.Debugf("start observer for peer %s", o.Address) 90 | 91 | for { 92 | r, err := o.d.Recv() 93 | if err != nil { 94 | errorCh <- err 95 | } 96 | 97 | if r == nil { 98 | errorCh <- errors.Errorf("received nil message, but expect a valid block instead. You could look into your peer logs for more info") 99 | return 100 | } 101 | 102 | fb := r.Type.(*peer.DeliverResponse_FilteredBlock) 103 | for _, b := range fb.FilteredBlock.FilteredTransactions { 104 | basic.LogEvent(o.logger, b.Txid, "CommitAtPeer") 105 | tapeSpan := basic.GetGlobalSpan() 106 | tapeSpan.FinishWithMap(b.Txid, o.Address, basic.COMMIT_AT_PEER) 107 | } 108 | o.logger.Debugf("receivedTime %8.2fs\tBlock %6d\tTx %6d\t Address %s\n", time.Since(now).Seconds(), fb.FilteredBlock.Number, len(fb.FilteredBlock.FilteredTransactions), o.Address) 109 | 110 | blockCh <- &AddressedBlock{fb.FilteredBlock, o.index, time.Since(now)} 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/generatorFactory.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperledger-twgc/tape/pkg/infra" 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type TrafficGenerator struct { 13 | ctx context.Context 14 | crypto infra.Crypto 15 | raw chan *basic.TracingProposal 16 | signed []chan *basic.Elements 17 | envs chan *basic.TracingEnvelope 18 | processed chan *basic.Elements 19 | config basic.Config 20 | num int 21 | burst int 22 | signerNumber int 23 | parallel int 24 | rate float64 25 | logger *log.Logger 26 | errorCh chan error 27 | } 28 | 29 | func NewTrafficGenerator(ctx context.Context, crypto infra.Crypto, envs chan *basic.TracingEnvelope, raw chan *basic.TracingProposal, processed chan *basic.Elements, signed []chan *basic.Elements, config basic.Config, num int, burst, signerNumber, parallel int, rate float64, logger *log.Logger, errorCh chan error) *TrafficGenerator { 30 | return &TrafficGenerator{ 31 | ctx: ctx, 32 | crypto: crypto, 33 | raw: raw, 34 | signed: signed, 35 | envs: envs, 36 | processed: processed, 37 | config: config, 38 | num: num, 39 | parallel: parallel, 40 | burst: burst, 41 | signerNumber: signerNumber, 42 | rate: rate, 43 | logger: logger, 44 | errorCh: errorCh, 45 | } 46 | } 47 | 48 | // table | proposal boradcaster fake 49 | // full process | 1 1 0 6 50 | // commit | 0 1 1 3 51 | // query | 1 0 0 4 52 | func (t *TrafficGenerator) CreateGeneratorWorkers(mode int) ([]infra.Worker, error) { 53 | generator_workers := make([]infra.Worker, 0) 54 | // if create proposers int/4 = 1 55 | if mode/infra.PROPOSALFILTER == 1 { 56 | proposers, err := CreateProposers(t.ctx, t.signed, t.processed, t.config, t.logger) 57 | if err != nil { 58 | return generator_workers, err 59 | } 60 | generator_workers = append(generator_workers, proposers) 61 | assembler := &Assembler{Signer: t.crypto, Ctx: t.ctx, Raw: t.raw, Signed: t.signed, ErrorCh: t.errorCh, Logger: t.logger} 62 | Integrator := &Integrator{Signer: t.crypto, Ctx: t.ctx, Processed: t.processed, Envs: t.envs, ErrorCh: t.errorCh, Logger: t.logger} 63 | for i := 0; i < t.signerNumber; i++ { 64 | generator_workers = append(generator_workers, assembler) 65 | generator_workers = append(generator_workers, Integrator) 66 | } 67 | } 68 | // if boradcaster int mod 3 = 0 69 | if mode%infra.COMMITFILTER == 0 { 70 | broadcaster, err := CreateBroadcasters(t.ctx, t.envs, t.errorCh, t.config, t.logger) 71 | if err != nil { 72 | return generator_workers, err 73 | } 74 | generator_workers = append(generator_workers, broadcaster) 75 | } 76 | // if not fake int mod 2 = 0 77 | for i := 0; i < t.parallel; i++ { 78 | if mode%infra.QUERYFILTER == 0 { 79 | Initiator := &Initiator{Num: t.num, Burst: t.burst, R: t.rate, Config: t.config, Crypto: t.crypto, Logger: t.logger, Raw: t.raw, ErrorCh: t.errorCh} 80 | generator_workers = append(generator_workers, Initiator) 81 | } else { 82 | // if fake int mod 2 = 1 83 | fackEnvelopGenerator := &FackEnvelopGenerator{Num: t.num, Burst: t.burst, R: t.rate, Config: t.config, Crypto: t.crypto, Envs: t.envs, ErrorCh: t.errorCh} 84 | generator_workers = append(generator_workers, fackEnvelopGenerator) 85 | } 86 | } 87 | return generator_workers, nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/policyHandler_test.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator_test 2 | 3 | import ( 4 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 5 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | const org1 = "org1" 11 | const org2 = "org2" 12 | 13 | var _ = Describe("PolicyHandler", func() { 14 | It("should pass when org1 with rule org1 or org2", func() { 15 | input := make([]string, 2) 16 | input[0] = org1 17 | 18 | //input[1] = "org2" 19 | rule := `package tape 20 | 21 | default allow = false 22 | allow { 23 | input[_] == "` + org1 + `" 24 | } 25 | 26 | allow { 27 | input[_] == "` + org2 + `" 28 | }` 29 | 30 | instance := &basic.Elements{ 31 | Orgs: input, 32 | } 33 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(rs).To(BeTrue()) 36 | }) 37 | 38 | It("should not pass when null with rule org1 or org2", func() { 39 | input := make([]string, 2) 40 | rule := `package tape 41 | 42 | default allow = false 43 | allow { 44 | input[_] == "` + org1 + `" 45 | } 46 | 47 | allow { 48 | input[_] == "` + org2 + `" 49 | }` 50 | instance := &basic.Elements{ 51 | Orgs: input, 52 | } 53 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 54 | Expect(err).NotTo(HaveOccurred()) 55 | Expect(rs).To(BeFalse()) 56 | }) 57 | 58 | It("should not pass when org1 with rule org1 and org2", func() { 59 | input := make([]string, 2) 60 | input[0] = "org1" 61 | rule := `package tape 62 | 63 | default allow = false 64 | allow { 65 | input[_] == "` + org1 + `" 66 | input[_] == "` + org2 + `" 67 | }` 68 | instance := &basic.Elements{ 69 | Orgs: input, 70 | } 71 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 72 | Expect(err).NotTo(HaveOccurred()) 73 | Expect(rs).To(BeFalse()) 74 | }) 75 | 76 | It("should pass with rule org1 and org2", func() { 77 | input := make([]string, 2) 78 | input[0] = "org1" 79 | input[1] = "org2" 80 | rule := `package tape 81 | 82 | default allow = false 83 | allow { 84 | input[_] == "` + org1 + `" 85 | input[_] == "` + org2 + `" 86 | } 87 | ` 88 | instance := &basic.Elements{ 89 | Orgs: input, 90 | } 91 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 92 | Expect(err).NotTo(HaveOccurred()) 93 | Expect(rs).To(BeTrue()) 94 | }) 95 | 96 | It("should pass with rule org1 and org2", func() { 97 | input := make([]string, 2) 98 | input[0] = org1 99 | input[1] = org2 100 | rule := `package tape 101 | 102 | default allow = false 103 | allow { 104 | 1 == 1 105 | } 106 | ` 107 | instance := &basic.Elements{ 108 | Orgs: input, 109 | } 110 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 111 | Expect(err).NotTo(HaveOccurred()) 112 | Expect(rs).To(BeTrue()) 113 | }) 114 | 115 | It("Same instance can't pass twice", func() { 116 | input := make([]string, 2) 117 | input[0] = org1 118 | input[1] = org2 119 | rule := `package tape 120 | 121 | default allow = false 122 | allow { 123 | input[_] == "` + org1 + `" 124 | input[_] == "` + org2 + `" 125 | } 126 | ` 127 | instance := &basic.Elements{ 128 | Orgs: input, 129 | } 130 | rs, err := trafficGenerator.CheckPolicy(instance, rule) 131 | Expect(err).NotTo(HaveOccurred()) 132 | Expect(rs).To(BeTrue()) 133 | 134 | rs, err = trafficGenerator.CheckPolicy(instance, rule) 135 | Expect(err).NotTo(HaveOccurred()) 136 | Expect(rs).To(BeFalse()) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /e2e/util.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "math/big" 10 | "net" 11 | "os" 12 | "text/template" 13 | "time" 14 | ) 15 | 16 | type NodeSpec struct { 17 | Addr string 18 | MtlsCrt string 19 | MtlsKey string 20 | Mtls bool 21 | } 22 | 23 | type Values struct { 24 | PrivSk string 25 | SignCert string 26 | MtlsCrt string 27 | MtlsKey string 28 | Mtls bool 29 | PeersAddrs []string 30 | OrdererAddr string 31 | PeersNodeSpecs []NodeSpec 32 | CommitThreshold int 33 | PolicyFile string 34 | } 35 | 36 | func (va Values) Load() Values { 37 | va.PeersNodeSpecs = make([]NodeSpec, 0) 38 | for _, v := range va.PeersAddrs { 39 | node := NodeSpec{ 40 | Addr: v, 41 | MtlsCrt: va.MtlsCrt, 42 | MtlsKey: va.MtlsKey, 43 | Mtls: va.Mtls, 44 | } 45 | va.PeersNodeSpecs = append(va.PeersNodeSpecs, node) 46 | } 47 | return va 48 | } 49 | 50 | func GeneratePolicy(policyFile *os.File) error { 51 | _, err := policyFile.Write([]byte(`package tape 52 | 53 | default allow = false 54 | allow { 55 | 1 == 1 56 | }`)) 57 | return err 58 | } 59 | 60 | func GenerateCertAndKeys(key, cert *os.File) error { 61 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 62 | if err != nil { 63 | return err 64 | } 65 | privDer, err := x509.MarshalPKCS8PrivateKey(priv) 66 | if err != nil { 67 | return err 68 | } 69 | err = pem.Encode(key, &pem.Block{Type: "PRIVATE KEY", Bytes: privDer}) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | template := &x509.Certificate{ 75 | SerialNumber: new(big.Int), 76 | NotAfter: time.Now().Add(time.Hour), 77 | IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, 78 | } 79 | 80 | certDer, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv) 81 | if err != nil { 82 | return err 83 | } 84 | err = pem.Encode(cert, &pem.Block{Type: "CERTIFICATE", Bytes: certDer}) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | func GenerateConfigFile(fileName string, values Values) { 92 | // {{range $k, $v := .Var}} {{$k}} => {{$v}} {{end}} 93 | values = values.Load() 94 | var Text = `# Definition of nodes 95 | {{range $k, $v := .PeersNodeSpecs}} 96 | node: &node{{$k}} 97 | addr: {{ .Addr }}{{ if .Mtls }} 98 | tls_ca_cert: {{.MtlsCrt}} 99 | tls_ca_key: {{.MtlsKey}} 100 | tls_ca_root: {{.MtlsCrt}} 101 | {{ end }} 102 | {{ end }} 103 | orderer1: &orderer1 104 | addr: {{ .OrdererAddr }}{{ if .Mtls }} 105 | tls_ca_cert: {{.MtlsCrt}} 106 | tls_ca_key: {{.MtlsKey}} 107 | tls_ca_root: {{.MtlsCrt}} 108 | {{ end }} 109 | # Nodes to interact with 110 | endorsers:{{range $k, $v := .PeersNodeSpecs}} 111 | - *node{{$k}}{{end}} 112 | committers: {{range $k, $v := .PeersNodeSpecs}} 113 | - *node{{$k}}{{end}} 114 | commitThreshold: {{ .CommitThreshold }} 115 | orderer: *orderer1 116 | policyFile: {{ .PolicyFile }} 117 | channel: test-channel 118 | chaincode: test-chaincode 119 | mspid: Org1MSP 120 | private_key: {{.PrivSk}} 121 | sign_cert: {{.SignCert}} 122 | num_of_conn: 10 123 | client_per_conn: 10 124 | ` 125 | var err error 126 | var tmpl *template.Template 127 | tmpl, err = template.New("test").Parse(Text) 128 | if err != nil { 129 | panic(err) 130 | } 131 | file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0755) 132 | if err != nil { 133 | panic(err) 134 | } 135 | defer file.Close() 136 | err = tmpl.Execute(file, values) 137 | if err != nil { 138 | panic(err) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /pkg/infra/basic/client.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | 7 | "github.com/hyperledger-twgc/tape/internal/fabric/core/comm" 8 | 9 | "github.com/hyperledger/fabric-protos-go-apiv2/orderer" 10 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | func CreateGRPCClient(node Node) (*comm.GRPCClient, error) { 17 | var certs [][]byte 18 | if node.TLSCACertByte != nil { 19 | certs = append(certs, node.TLSCACertByte) 20 | } 21 | config := comm.ClientConfig{} 22 | config.SecOpts = comm.SecureOptions{ 23 | UseTLS: false, 24 | RequireClientCert: false, 25 | ServerRootCAs: certs, 26 | } 27 | 28 | if len(certs) > 0 { 29 | config.SecOpts.UseTLS = true 30 | if len(node.TLSCAKey) > 0 && len(node.TLSCARoot) > 0 { 31 | config.SecOpts.RequireClientCert = true 32 | config.SecOpts.Certificate = node.TLSCACertByte 33 | config.SecOpts.Key = node.TLSCAKeyByte 34 | if node.TLSCARootByte != nil { 35 | config.SecOpts.ClientRootCAs = append(config.SecOpts.ClientRootCAs, node.TLSCARootByte) 36 | } 37 | } 38 | } 39 | 40 | grpcClient, err := comm.NewGRPCClient(config) 41 | //to do: unit test for this error, current fails to make case for this 42 | if err != nil { 43 | return nil, errors.Wrapf(err, "error connecting to %s", node.Addr) 44 | } 45 | 46 | return grpcClient, nil 47 | } 48 | 49 | func CreateEndorserClient(node Node, logger *log.Logger) (peer.EndorserClient, error) { 50 | conn, err := DialConnection(node, logger) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return peer.NewEndorserClient(conn), nil 55 | } 56 | 57 | func CreateBroadcastClient(ctx context.Context, node Node, logger *log.Logger) (orderer.AtomicBroadcast_BroadcastClient, error) { 58 | conn, err := DialConnection(node, logger) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return orderer.NewAtomicBroadcastClient(conn).Broadcast(ctx) 63 | } 64 | 65 | func CreateDeliverFilteredClient(ctx context.Context, node Node, logger *log.Logger) (peer.Deliver_DeliverFilteredClient, error) { 66 | conn, err := DialConnection(node, logger) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return peer.NewDeliverClient(conn).DeliverFiltered(ctx) 71 | } 72 | 73 | // TODO: use a global get logger function instead inject a logger 74 | func DialConnection(node Node, logger *log.Logger) (*grpc.ClientConn, error) { 75 | gRPCClient, err := CreateGRPCClient(node) 76 | if err != nil { 77 | return nil, err 78 | } 79 | var connError error 80 | var conn *grpc.ClientConn 81 | for i := 1; i <= 3; i++ { 82 | conn, connError = gRPCClient.NewConnection(node.Addr, func(tlsConfig *tls.Config) { 83 | tlsConfig.InsecureSkipVerify = true 84 | tlsConfig.ServerName = node.SslTargetNameOverride 85 | }) 86 | if connError == nil { 87 | return conn, nil 88 | } else { 89 | logger.Errorf("%d of 3 attempts to make connection to %s, details: %s", i, node.Addr, connError) 90 | } 91 | } 92 | return nil, errors.Wrapf(connError, "error connecting to %s", node.Addr) 93 | } 94 | 95 | func CreateDeliverClient(node Node) (orderer.AtomicBroadcast_DeliverClient, error) { 96 | gRPCClient, err := CreateGRPCClient(node) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | conn, err := gRPCClient.NewConnection(node.Addr, func(tlsConfig *tls.Config) { 102 | tlsConfig.InsecureSkipVerify = true 103 | tlsConfig.ServerName = node.SslTargetNameOverride 104 | }) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return orderer.NewAtomicBroadcastClient(conn).Deliver(context.Background()) 109 | } 110 | -------------------------------------------------------------------------------- /e2e/multi_peer_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/e2e/mock" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | . "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var _ = Describe("Mock test for good path", func() { 17 | 18 | Context("E2E with multi mocked Fabric", func() { 19 | 20 | When("TLS is disabled", func() { 21 | It("should work properly", func() { 22 | server, err := mock.NewServer(2, nil) 23 | Expect(err).NotTo(HaveOccurred()) 24 | server.Start() 25 | defer server.Stop() 26 | 27 | config, err := os.CreateTemp("", "no-tls-config-*.yaml") 28 | Expect(err).NotTo(HaveOccurred()) 29 | paddrs, oaddr := server.Addresses() 30 | configValue := e2e.Values{ 31 | PrivSk: mtlsKeyFile.Name(), 32 | SignCert: mtlsCertFile.Name(), 33 | Mtls: false, 34 | PeersAddrs: paddrs, 35 | OrdererAddr: oaddr, 36 | CommitThreshold: 1, 37 | PolicyFile: PolicyFile.Name(), 38 | } 39 | e2e.GenerateConfigFile(config.Name(), configValue) 40 | 41 | cmd := exec.Command(tapeBin, "-c", config.Name(), "-n", "500", "--parallel", "5") 42 | tapeSession, err = gexec.Start(cmd, nil, nil) 43 | Expect(err).NotTo(HaveOccurred()) 44 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*10.*")) 45 | }) 46 | }) 47 | 48 | When("TLS is disabled", func() { 49 | It("should work properly", func() { 50 | server, err := mock.NewServer(2, nil) 51 | Expect(err).NotTo(HaveOccurred()) 52 | server.Start() 53 | defer server.Stop() 54 | 55 | config, err := os.CreateTemp("", "no-tls-config-*.yaml") 56 | Expect(err).NotTo(HaveOccurred()) 57 | paddrs, oaddr := server.Addresses() 58 | configValue := e2e.Values{ 59 | PrivSk: mtlsKeyFile.Name(), 60 | SignCert: mtlsCertFile.Name(), 61 | Mtls: false, 62 | PeersAddrs: paddrs, 63 | OrdererAddr: oaddr, 64 | CommitThreshold: 2, 65 | PolicyFile: PolicyFile.Name(), 66 | } 67 | e2e.GenerateConfigFile(config.Name(), configValue) 68 | 69 | cmd := exec.Command(tapeBin, "-c", config.Name(), "-n", "500", "--signers", "2") 70 | tapeSession, err = gexec.Start(cmd, nil, nil) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*10.*")) 73 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*10.*")) 74 | }) 75 | 76 | It("should work properly without number", func() { 77 | server, err := mock.NewServer(2, nil) 78 | Expect(err).NotTo(HaveOccurred()) 79 | server.Start() 80 | defer server.Stop() 81 | 82 | config, err := os.CreateTemp("", "no-tls-config-*.yaml") 83 | Expect(err).NotTo(HaveOccurred()) 84 | paddrs, oaddr := server.Addresses() 85 | configValue := e2e.Values{ 86 | PrivSk: mtlsKeyFile.Name(), 87 | SignCert: mtlsCertFile.Name(), 88 | Mtls: false, 89 | PeersAddrs: paddrs, 90 | OrdererAddr: oaddr, 91 | CommitThreshold: 2, 92 | PolicyFile: PolicyFile.Name(), 93 | } 94 | e2e.GenerateConfigFile(config.Name(), configValue) 95 | 96 | cmd := exec.Command(tapeBin, "-c", config.Name(), "--signers", "2") 97 | tapeSession, err = gexec.Start(cmd, nil, nil) 98 | Expect(err).NotTo(HaveOccurred()) 99 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*10.*")) 100 | Eventually(tapeSession.Out).Should(Say("Time.*Block.*Tx.*10.*")) 101 | }) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/proposer.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | 9 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type Proposers struct { 14 | workers [][]*Proposer 15 | logger *log.Logger 16 | ctx context.Context 17 | signed []chan *basic.Elements 18 | processed chan *basic.Elements 19 | config basic.Config 20 | } 21 | 22 | func CreateProposers(ctx context.Context, signed []chan *basic.Elements, processed chan *basic.Elements, config basic.Config, logger *log.Logger) (*Proposers, error) { 23 | var ps [][]*Proposer 24 | var err error 25 | //one proposer per connection per peer 26 | for _, node := range config.Endorsers { 27 | row := make([]*Proposer, config.NumOfConn) 28 | for j := 0; j < config.NumOfConn; j++ { 29 | row[j], err = CreateProposer(node, logger, config.Rule) 30 | if err != nil { 31 | return nil, err 32 | } 33 | } 34 | ps = append(ps, row) 35 | } 36 | 37 | return &Proposers{workers: ps, logger: logger, ctx: ctx, signed: signed, processed: processed, config: config}, nil 38 | } 39 | 40 | func (ps *Proposers) Start() { 41 | ps.logger.Infof("Start sending transactions.") 42 | for i := 0; i < len(ps.config.Endorsers); i++ { 43 | // peer connection should be config.ClientPerConn * config.NumOfConn 44 | for k := 0; k < ps.config.ClientPerConn; k++ { 45 | for j := 0; j < ps.config.NumOfConn; j++ { 46 | go ps.workers[i][j].Start(ps.ctx, ps.signed[i], ps.processed) 47 | } 48 | } 49 | } 50 | } 51 | 52 | type Proposer struct { 53 | e peer.EndorserClient 54 | Addr string 55 | logger *log.Logger 56 | Org string 57 | rule string 58 | } 59 | 60 | func CreateProposer(node basic.Node, logger *log.Logger, rule string) (*Proposer, error) { 61 | if len(rule) == 0 { 62 | return nil, errors.New("empty endorsement policy") 63 | } 64 | endorser, err := basic.CreateEndorserClient(node, logger) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return &Proposer{e: endorser, Addr: node.Addr, logger: logger, Org: node.Org, rule: rule}, nil 69 | } 70 | 71 | func (p *Proposer) Start(ctx context.Context, signed, processed chan *basic.Elements) { 72 | tapeSpan := basic.GetGlobalSpan() 73 | for { 74 | select { 75 | case s := <-signed: 76 | //send sign proposal to peer for endorsement 77 | span := tapeSpan.MakeSpan(s.TxId, p.Addr, basic.ENDORSEMENT_AT_PEER, s.Span) 78 | r, err := p.e.ProcessProposal(ctx, s.SignedProp) 79 | if err != nil || r.Response.Status < 200 || r.Response.Status >= 400 { 80 | // end sending proposal 81 | if r == nil { 82 | p.logger.Errorf("Err processing proposal: %s, status: unknown, addr: %s \n", err, p.Addr) 83 | } else { 84 | p.logger.Errorf("Err processing proposal: %s, status: %d, message: %s, addr: %s \n", err, r.Response.Status, r.Response.Message, p.Addr) 85 | } 86 | continue 87 | } 88 | span.Finish() 89 | s.Lock.Lock() 90 | // if prometheus 91 | // report read readlatency with peer in label 92 | basic.GetLatencyMap().ReportReadLatency(s.TxId, p.Addr) 93 | s.Responses = append(s.Responses, r) 94 | s.Orgs = append(s.Orgs, p.Org) 95 | rs, err := CheckPolicy(s, p.rule) 96 | if err != nil { 97 | p.logger.Errorf("Fails to check rule of endorsement %s \n", err) 98 | } 99 | if rs { 100 | //if len(s.Responses) >= threshold { // from value upgrade to OPA logic 101 | s.EndorsementSpan.Finish() 102 | // OPA 103 | // if already in processed queue or after, ignore 104 | // if not, send into process queue 105 | processed <- s 106 | basic.LogEvent(p.logger, s.TxId, "CompletedCollectEndorsement") 107 | } 108 | s.Lock.Unlock() 109 | case <-ctx.Done(): 110 | return 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/infra/cmdImpl/fullProcess.go: -------------------------------------------------------------------------------- 1 | package cmdImpl 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/hyperledger-twgc/tape/pkg/infra" 12 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/promhttp" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | func Process(configPath string, num int, burst, signerNumber, parallel int, rate float64, enablePrometheus bool, prometheusAddr string, logger *log.Logger, processmod int) error { 19 | /*** signal ***/ 20 | c := make(chan os.Signal, 1) 21 | 22 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 23 | /*** variables ***/ 24 | cmdConfig, err := CreateCmd(configPath, num, burst, signerNumber, parallel, rate, logger) 25 | if err != nil { 26 | return err 27 | } 28 | defer cmdConfig.cancel() 29 | defer cmdConfig.Closer.Close() 30 | var Observer_workers []infra.Worker 31 | var Observers infra.ObserverWorker 32 | basic.SetMod(processmod) 33 | /*** workers ***/ 34 | if processmod != infra.TRAFFIC { 35 | Observer_workers, Observers, err = cmdConfig.Observerfactory.CreateObserverWorkers(processmod) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | var generator_workers []infra.Worker 41 | if processmod != infra.OBSERVER { 42 | if processmod == infra.TRAFFIC { 43 | generator_workers, err = cmdConfig.Generator.CreateGeneratorWorkers(processmod - 1) 44 | if err != nil { 45 | return err 46 | } 47 | } else { 48 | generator_workers, err = cmdConfig.Generator.CreateGeneratorWorkers(processmod) 49 | if err != nil { 50 | return err 51 | } 52 | } 53 | } 54 | 55 | var transactionlatency, readlatency *prometheus.SummaryVec 56 | /*** start prometheus ***/ 57 | if enablePrometheus { 58 | go func() { 59 | fmt.Println("start prometheus") 60 | http.Handle("/metrics", promhttp.Handler()) 61 | server := &http.Server{Addr: prometheusAddr, Handler: nil} 62 | err = server.ListenAndServe() 63 | if err != nil { 64 | cmdConfig.ErrorCh <- err 65 | } 66 | }() 67 | 68 | transactionlatency = prometheus.NewSummaryVec( 69 | prometheus.SummaryOpts{ 70 | Name: "transaction_latency_duration", 71 | Help: "Transaction latency distributions.", 72 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 73 | }, 74 | []string{"transactionlatency"}, 75 | ) 76 | 77 | readlatency = prometheus.NewSummaryVec( 78 | prometheus.SummaryOpts{ 79 | Name: "read_latency_duration", 80 | Help: "Read latency distributions.", 81 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 82 | }, 83 | []string{"readlatency"}, 84 | ) 85 | 86 | prometheus.MustRegister(transactionlatency) 87 | prometheus.MustRegister(readlatency) 88 | 89 | basic.InitLatencyMap(transactionlatency, readlatency, processmod, enablePrometheus) 90 | } 91 | 92 | /*** start workers ***/ 93 | for _, worker := range Observer_workers { 94 | go worker.Start() 95 | } 96 | for _, worker := range generator_workers { 97 | go worker.Start() 98 | } 99 | /*** waiting for complete ***/ 100 | total := num * parallel 101 | for { 102 | select { 103 | case err = <-cmdConfig.ErrorCh: 104 | fmt.Println("For FAQ, please check https://github.com/Hyperledger-TWGC/tape/wiki/FAQ") 105 | return err 106 | case <-cmdConfig.FinishCh: 107 | duration := time.Since(Observers.GetTime()) 108 | logger.Infof("Completed processing transactions.") 109 | fmt.Printf("tx: %d, duration: %+v, tps: %f\n", total, duration, float64(total)/duration.Seconds()) 110 | return nil 111 | case s := <-c: 112 | fmt.Println("Stopped by signal received" + s.String()) 113 | fmt.Println("Completed processing transactions") 114 | return nil 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/fabric/protoutil/commonutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package protoutil 8 | 9 | import ( 10 | "crypto/rand" 11 | "time" 12 | 13 | "github.com/golang/protobuf/ptypes/timestamp" 14 | cb "github.com/hyperledger/fabric-protos-go-apiv2/common" 15 | "github.com/pkg/errors" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | // MarshalOrPanic serializes a protobuf message and panics if this 20 | // operation fails 21 | func MarshalOrPanic(pb proto.Message) []byte { 22 | data, err := proto.Marshal(pb) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return data 27 | } 28 | 29 | // Marshal serializes a protobuf message. 30 | func Marshal(pb proto.Message) ([]byte, error) { 31 | return proto.Marshal(pb) 32 | } 33 | 34 | // CreateNonce generates a nonce using the common/crypto package. 35 | func CreateNonce() ([]byte, error) { 36 | nonce, err := getRandomNonce() 37 | return nonce, errors.WithMessage(err, "error generating random nonce") 38 | } 39 | 40 | // ExtractEnvelope retrieves the requested envelope from a given block and 41 | // unmarshals it 42 | func ExtractEnvelope(block *cb.Block, index int) (*cb.Envelope, error) { 43 | if block.Data == nil { 44 | return nil, errors.New("block data is nil") 45 | } 46 | 47 | envelopeCount := len(block.Data.Data) 48 | if index < 0 || index >= envelopeCount { 49 | return nil, errors.New("envelope index out of bounds") 50 | } 51 | marshaledEnvelope := block.Data.Data[index] 52 | envelope, err := GetEnvelopeFromBlock(marshaledEnvelope) 53 | err = errors.WithMessagef(err, "block data does not carry an envelope at index %d", index) 54 | return envelope, err 55 | } 56 | 57 | // MakeChannelHeader creates a ChannelHeader. 58 | func MakeChannelHeader(headerType cb.HeaderType, version int32, chainID string, epoch uint64) *cb.ChannelHeader { 59 | return &cb.ChannelHeader{ 60 | Type: int32(headerType), 61 | Version: version, 62 | Timestamp: ×tamp.Timestamp{ 63 | Seconds: time.Now().Unix(), 64 | Nanos: 0, 65 | }, 66 | ChannelId: chainID, 67 | Epoch: epoch, 68 | } 69 | } 70 | 71 | // MakePayloadHeader creates a Payload Header. 72 | func MakePayloadHeader(ch *cb.ChannelHeader, sh *cb.SignatureHeader) *cb.Header { 73 | return &cb.Header{ 74 | ChannelHeader: MarshalOrPanic(ch), 75 | SignatureHeader: MarshalOrPanic(sh), 76 | } 77 | } 78 | 79 | // NewSignatureHeader returns a SignatureHeader with a valid nonce. 80 | func NewSignatureHeader(id Signer) (*cb.SignatureHeader, error) { 81 | creator, err := id.Serialize() 82 | if err != nil { 83 | return nil, err 84 | } 85 | nonce, err := CreateNonce() 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return &cb.SignatureHeader{ 91 | Creator: creator, 92 | Nonce: nonce, 93 | }, nil 94 | } 95 | 96 | // ChannelHeader returns the *cb.ChannelHeader for a given *cb.Envelope. 97 | func ChannelHeader(env *cb.Envelope) (*cb.ChannelHeader, error) { 98 | envPayload, err := UnmarshalPayload(env.Payload) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | if envPayload.Header == nil { 104 | return nil, errors.New("header not set") 105 | } 106 | 107 | if envPayload.Header.ChannelHeader == nil { 108 | return nil, errors.New("channel header not set") 109 | } 110 | 111 | chdr, err := UnmarshalChannelHeader(envPayload.Header.ChannelHeader) 112 | if err != nil { 113 | return nil, errors.WithMessage(err, "error unmarshaling channel header") 114 | } 115 | 116 | return chdr, nil 117 | } 118 | 119 | // ChannelID returns the Channel ID for a given *cb.Envelope. 120 | func ChannelID(env *cb.Envelope) (string, error) { 121 | chdr, err := ChannelHeader(env) 122 | if err != nil { 123 | return "", errors.WithMessage(err, "error retrieving channel header") 124 | } 125 | 126 | return chdr.ChannelId, nil 127 | } 128 | 129 | func getRandomNonce() ([]byte, error) { 130 | key := make([]byte, 24) 131 | 132 | _, err := rand.Read(key) 133 | if err != nil { 134 | return nil, errors.Wrap(err, "error getting random bytes") 135 | } 136 | return key, nil 137 | } 138 | -------------------------------------------------------------------------------- /pkg/infra/observer/benchmark_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package observer_test 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/opentracing/opentracing-go" 12 | 13 | "github.com/hyperledger-twgc/tape/e2e/mock" 14 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 15 | "github.com/hyperledger-twgc/tape/pkg/infra/observer" 16 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 17 | 18 | "github.com/google/uuid" 19 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func StartProposer(ctx context.Context, signed, processed chan *basic.Elements, logger *log.Logger, threshold int, addr string) { 24 | peer := basic.Node{ 25 | Addr: addr, 26 | } 27 | tr, closer := basic.Init("test") 28 | defer closer.Close() 29 | opentracing.SetGlobalTracer(tr) 30 | rule := ` 31 | package tape 32 | 33 | default allow = false 34 | 35 | allow { 36 | 1 == 1 37 | } 38 | ` 39 | Proposer, _ := trafficGenerator.CreateProposer(peer, logger, rule) 40 | go Proposer.Start(ctx, signed, processed) 41 | } 42 | 43 | func benchmarkNPeer(concurrency int, b *testing.B) { 44 | processed := make(chan *basic.Elements, 10) 45 | signeds := make([]chan *basic.Elements, concurrency) 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | logger := log.New() 48 | defer cancel() 49 | for i := 0; i < concurrency; i++ { 50 | signeds[i] = make(chan *basic.Elements, 10) 51 | mockpeer, err := mock.NewServer(1, nil) 52 | if err != nil { 53 | b.Fatal(err) 54 | } 55 | mockpeer.Start() 56 | defer mockpeer.Stop() 57 | StartProposer(ctx, signeds[i], processed, logger, concurrency, mockpeer.PeersAddresses()[0]) 58 | } 59 | b.ReportAllocs() 60 | b.ResetTimer() 61 | go func() { 62 | for i := 0; i < b.N; i++ { 63 | uuid, _ := uuid.NewRandom() 64 | span := opentracing.GlobalTracer().StartSpan("start transaction process", opentracing.Tag{Key: "txid", Value: uuid.String()}) 65 | ed_span := opentracing.GlobalTracer().StartSpan("endorsement", opentracing.Tag{Key: "txid", Value: uuid.String()}) 66 | data := &basic.Elements{SignedProp: &peer.SignedProposal{}, TxId: uuid.String(), Span: span, EndorsementSpan: ed_span} 67 | for _, s := range signeds { 68 | s <- data 69 | } 70 | } 71 | }() 72 | var n int 73 | for n < b.N { 74 | <-processed 75 | n++ 76 | } 77 | b.StopTimer() 78 | } 79 | 80 | func BenchmarkPeerEndorsement1(b *testing.B) { benchmarkNPeer(1, b) } 81 | func BenchmarkPeerEndorsement2(b *testing.B) { benchmarkNPeer(2, b) } 82 | func BenchmarkPeerEndorsement4(b *testing.B) { benchmarkNPeer(4, b) } 83 | func BenchmarkPeerEndorsement8(b *testing.B) { benchmarkNPeer(8, b) } 84 | 85 | func benchmarkAsyncCollector(concurrent int, b *testing.B) { 86 | block := make(chan *observer.AddressedBlock, 100) 87 | done := make(chan struct{}) 88 | logger := log.New() 89 | 90 | var once sync.Once 91 | instance, _ := observer.NewBlockCollector(concurrent, concurrent, context.Background(), block, done, b.N, false, logger, &once, true) 92 | go instance.Start() 93 | 94 | b.ReportAllocs() 95 | b.ResetTimer() 96 | for i := 0; i < concurrent; i++ { 97 | go func(idx int) { 98 | for j := 0; j < b.N; j++ { 99 | uuid, _ := uuid.NewRandom() 100 | FilteredTransactions := make([]*peer.FilteredTransaction, 0) 101 | FilteredTransactions = append(FilteredTransactions, &peer.FilteredTransaction{Txid: uuid.String()}) 102 | data := &observer.AddressedBlock{Address: idx, FilteredBlock: &peer.FilteredBlock{Number: uint64(j), FilteredTransactions: FilteredTransactions}} 103 | block <- data 104 | } 105 | }(i) 106 | } 107 | <-done 108 | b.StopTimer() 109 | } 110 | 111 | func BenchmarkAsyncCollector1(b *testing.B) { benchmarkAsyncCollector(1, b) } 112 | func BenchmarkAsyncCollector2(b *testing.B) { benchmarkAsyncCollector(2, b) } 113 | func BenchmarkAsyncCollector4(b *testing.B) { benchmarkAsyncCollector(4, b) } 114 | func BenchmarkAsyncCollector8(b *testing.B) { benchmarkAsyncCollector(8, b) } 115 | func BenchmarkAsyncCollector16(b *testing.B) { benchmarkAsyncCollector(16, b) } 116 | -------------------------------------------------------------------------------- /pkg/infra/observer/observerFactory.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type ObserverFactory struct { 15 | config basic.Config 16 | crypto infra.Crypto 17 | blockCh chan *AddressedBlock 18 | logger *log.Logger 19 | ctx context.Context 20 | finishCh chan struct{} 21 | num int 22 | parallel int 23 | envs chan *basic.TracingEnvelope 24 | errorCh chan error 25 | } 26 | 27 | func NewObserverFactory(config basic.Config, 28 | crypto infra.Crypto, 29 | blockCh chan *AddressedBlock, 30 | logger *log.Logger, 31 | ctx context.Context, 32 | finishCh chan struct{}, 33 | num, parallel int, 34 | envs chan *basic.TracingEnvelope, 35 | errorCh chan error) *ObserverFactory { 36 | return &ObserverFactory{config, 37 | crypto, 38 | blockCh, 39 | logger, 40 | ctx, 41 | finishCh, 42 | num, 43 | parallel, 44 | envs, 45 | errorCh, 46 | } 47 | } 48 | 49 | func (of *ObserverFactory) CreateObserverWorkers(mode int) ([]infra.Worker, infra.ObserverWorker, error) { 50 | switch mode { 51 | case infra.ENDORSEMENT: 52 | return of.CreateEndorsementObserverWorkers() 53 | case infra.OBSERVER: 54 | return of.CreateFullProcessObserverWorkers() 55 | case infra.COMMIT: 56 | return of.CreateCommitObserverWorkers() 57 | default: 58 | return of.CreateFullProcessObserverWorkers() 59 | } 60 | } 61 | 62 | // 6 63 | func (of *ObserverFactory) CreateFullProcessObserverWorkers() ([]infra.Worker, infra.ObserverWorker, error) { 64 | observer_workers := make([]infra.Worker, 0) 65 | total := of.parallel * of.num 66 | var once sync.Once 67 | blockCollector, err := NewBlockCollector(of.config.CommitThreshold, len(of.config.Committers), of.ctx, of.blockCh, of.finishCh, total, true, of.logger, &once, true) 68 | if err != nil { 69 | return observer_workers, nil, errors.Wrap(err, "failed to create block collector") 70 | } 71 | observer_workers = append(observer_workers, blockCollector) 72 | observers, err := CreateObservers(of.ctx, of.crypto, of.errorCh, of.blockCh, of.config, of.logger) 73 | if err != nil { 74 | return observer_workers, observers, err 75 | } 76 | observer_workers = append(observer_workers, observers) 77 | cryptoImpl, err := of.config.LoadCrypto() 78 | if err != nil { 79 | return observer_workers, observers, err 80 | } 81 | EndorseObserverWorker, err := CreateCommitObserver(of.config.Channel, 82 | of.config.Orderer, 83 | cryptoImpl, 84 | of.logger, 85 | total, 86 | of.config, 87 | of.errorCh, 88 | of.finishCh, 89 | &once, 90 | false) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | observer_workers = append(observer_workers, EndorseObserverWorker) 95 | return observer_workers, observers, nil 96 | } 97 | 98 | // 4 99 | func (of *ObserverFactory) CreateEndorsementObserverWorkers() ([]infra.Worker, infra.ObserverWorker, error) { 100 | observer_workers := make([]infra.Worker, 0) 101 | total := of.parallel * of.num 102 | var once sync.Once 103 | EndorseObserverWorker := CreateEndorseObserver(of.envs, total, of.finishCh, &once, of.logger) 104 | observer_workers = append(observer_workers, EndorseObserverWorker) 105 | return observer_workers, EndorseObserverWorker, nil 106 | } 107 | 108 | // 3 109 | func (of *ObserverFactory) CreateCommitObserverWorkers() ([]infra.Worker, infra.ObserverWorker, error) { 110 | observer_workers := make([]infra.Worker, 0) 111 | cryptoImpl, err := of.config.LoadCrypto() 112 | if err != nil { 113 | return observer_workers, nil, err 114 | } 115 | var once sync.Once 116 | total := of.parallel * of.num 117 | EndorseObserverWorker, err := CreateCommitObserver(of.config.Channel, 118 | of.config.Orderer, 119 | cryptoImpl, 120 | of.logger, 121 | total, 122 | of.config, 123 | of.errorCh, 124 | of.finishCh, 125 | &once, 126 | true) 127 | if err != nil { 128 | return nil, nil, err 129 | } 130 | observer_workers = append(observer_workers, EndorseObserverWorker) 131 | return observer_workers, EndorseObserverWorker, nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/proposer_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package trafficGenerator_test 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/hyperledger-twgc/tape/e2e/mock" 11 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 12 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 13 | "github.com/opentracing/opentracing-go" 14 | 15 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 16 | . "github.com/onsi/ginkgo/v2" 17 | . "github.com/onsi/gomega" 18 | "github.com/onsi/gomega/gmeasure" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var _ = Describe("Proposer", func() { 23 | 24 | var addr string 25 | var logger = log.New() 26 | var processed chan *basic.Elements 27 | 28 | BeforeEach(func() { 29 | server, _ := mock.NewServer(1, nil) 30 | server.Start() 31 | addr = server.PeersAddresses()[0] 32 | }) 33 | 34 | Context("CreateProposer", func() { 35 | It("successfully creates a proposer", func() { 36 | dummy := basic.Node{ 37 | Addr: addr, 38 | } 39 | rule := "1 == 1" 40 | Proposer, err := trafficGenerator.CreateProposer(dummy, logger, rule) 41 | Expect(Proposer.Addr).To(Equal(addr)) 42 | Expect(err).NotTo(HaveOccurred()) 43 | }) 44 | }) 45 | 46 | Context("CreateBroadcasters", func() { 47 | It("successfully creates a broadcaster", func() { 48 | dummy := basic.Node{ 49 | Addr: addr, 50 | } 51 | Broadcaster, err := trafficGenerator.CreateBroadcaster(context.Background(), dummy, logger) 52 | Expect(Broadcaster).NotTo(BeNil()) 53 | Expect(err).NotTo(HaveOccurred()) 54 | }) 55 | 56 | It("captures connection errors", func() { 57 | dummy := basic.Node{ 58 | Addr: "invalid_addr", 59 | } 60 | _, err := trafficGenerator.CreateBroadcaster(context.Background(), dummy, logger) 61 | Expect(err).Should(MatchError(ContainSubstring("rpc error: code"))) 62 | }) 63 | }) 64 | 65 | Context("Tape should do less for prepare and summary endorsement process", func() { 66 | // 0.002 here for mac testing on azp 67 | // For ginkgo, 68 | // You may only call Measure from within a Describe, Context or When 69 | // So here only tested with concurrent as 8 peers 70 | It("it should do endorsement efficiently for 2 peers", Serial, Label("measurement"), func() { 71 | experiment := gmeasure.NewExperiment("Tape Peers") 72 | AddReportEntry(experiment.Name, experiment) 73 | 74 | tracer, closer := basic.Init("test") 75 | defer closer.Close() 76 | opentracing.SetGlobalTracer(tracer) 77 | span := opentracing.GlobalTracer().StartSpan("start transaction process") 78 | defer span.Finish() 79 | peerNum := 2 80 | processed = make(chan *basic.Elements, 10) 81 | ctx, cancel := context.WithCancel(context.Background()) 82 | defer cancel() 83 | signeds := make([]chan *basic.Elements, peerNum) 84 | mockpeer, err := mock.NewServer(peerNum, nil) 85 | Expect(err).NotTo(HaveOccurred()) 86 | mockpeer.Start() 87 | defer mockpeer.Stop() 88 | for i := 0; i < peerNum; i++ { 89 | signeds[i] = make(chan *basic.Elements, 10) 90 | StartProposer(ctx, signeds[i], processed, logger, peerNum, mockpeer.PeersAddresses()[i]) 91 | } 92 | 93 | experiment.Sample(func(idx int) { 94 | experiment.MeasureDuration("process", func() { 95 | data := &basic.Elements{SignedProp: &peer.SignedProposal{}, TxId: "123", Span: span, EndorsementSpan: span} 96 | for _, s := range signeds { 97 | s <- data 98 | } 99 | <-processed 100 | }) 101 | }, gmeasure.SamplingConfig{N: 100, Duration: time.Second}) 102 | 103 | repaginationStats := experiment.GetStats("process") 104 | medianDuration := repaginationStats.DurationFor(gmeasure.StatMedian) 105 | 106 | Expect(medianDuration).To(BeNumerically("<", 2*time.Millisecond)) 107 | }) 108 | }) 109 | }) 110 | 111 | func StartProposer(ctx context.Context, signed, processed chan *basic.Elements, logger *log.Logger, threshold int, addr string) { 112 | peer := basic.Node{ 113 | Addr: addr, 114 | } 115 | rule := ` 116 | package tape 117 | 118 | default allow = true 119 | ` 120 | Proposer, _ := trafficGenerator.CreateProposer(peer, logger, rule) 121 | go Proposer.Start(ctx, signed, processed) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/fackEnvelopGenerator_test.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator_test 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("FackEnvelopGenerator", func() { 15 | 16 | var ( 17 | configFile *os.File 18 | tmpDir string 19 | ) 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpDir, err = os.MkdirTemp("", "tape-") 24 | Expect(err).NotTo(HaveOccurred()) 25 | 26 | mtlsCertFile, err := os.CreateTemp(tmpDir, "mtls-*.crt") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | mtlsKeyFile, err := os.CreateTemp(tmpDir, "mtls-*.key") 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | err = e2e.GenerateCertAndKeys(mtlsKeyFile, mtlsCertFile) 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | PolicyFile, err := os.CreateTemp(tmpDir, "policy") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | err = e2e.GeneratePolicy(PolicyFile) 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | mtlsCertFile.Close() 42 | mtlsKeyFile.Close() 43 | PolicyFile.Close() 44 | 45 | configFile, err = os.CreateTemp(tmpDir, "config*.yaml") 46 | Expect(err).NotTo(HaveOccurred()) 47 | configValue := e2e.Values{ 48 | PrivSk: mtlsKeyFile.Name(), 49 | SignCert: mtlsCertFile.Name(), 50 | Mtls: false, 51 | PeersAddrs: []string{"dummy"}, 52 | OrdererAddr: "dummy", 53 | CommitThreshold: 1, 54 | PolicyFile: PolicyFile.Name(), 55 | } 56 | e2e.GenerateConfigFile(configFile.Name(), configValue) 57 | }) 58 | 59 | AfterEach(func() { 60 | os.RemoveAll(tmpDir) 61 | }) 62 | 63 | It("should crete proposal to raw without limit when limit is 0", func() { 64 | envs := make(chan *basic.TracingEnvelope, 1002) 65 | defer close(envs) 66 | errorCh := make(chan error, 1002) 67 | defer close(errorCh) 68 | config, err := basic.LoadConfig(configFile.Name()) 69 | Expect(err).NotTo(HaveOccurred()) 70 | crypto, err := config.LoadCrypto() 71 | Expect(err).NotTo(HaveOccurred()) 72 | t := time.Now() 73 | fackEnvelopGenerator := &trafficGenerator.FackEnvelopGenerator{ 74 | Num: 1002, 75 | Burst: 10, 76 | R: 0, 77 | Config: config, 78 | Crypto: crypto, 79 | Envs: envs, 80 | ErrorCh: errorCh, 81 | } 82 | fackEnvelopGenerator.Start() 83 | t1 := time.Now() 84 | Expect(envs).To(HaveLen(1002)) 85 | Expect(t1.Sub(t)).To(BeNumerically("<", 2*time.Second)) 86 | 87 | }) 88 | 89 | It("should crete proposal to raw with given limit bigger than 0 less than size", func() { 90 | envs := make(chan *basic.TracingEnvelope, 1002) 91 | defer close(envs) 92 | errorCh := make(chan error, 1002) 93 | defer close(errorCh) 94 | config, err := basic.LoadConfig(configFile.Name()) 95 | Expect(err).NotTo(HaveOccurred()) 96 | crypto, err := config.LoadCrypto() 97 | Expect(err).NotTo(HaveOccurred()) 98 | t := time.Now() 99 | fackEnvelopGenerator := &trafficGenerator.FackEnvelopGenerator{ 100 | Num: 12, 101 | Burst: 10, 102 | R: 1, 103 | Config: config, 104 | Crypto: crypto, 105 | Envs: envs, 106 | ErrorCh: errorCh, 107 | } 108 | fackEnvelopGenerator.Start() 109 | t1 := time.Now() 110 | Expect(envs).To(HaveLen(12)) 111 | Expect(t1.Sub(t)).To(BeNumerically("<", 2*time.Second)) 112 | }) 113 | 114 | It("should crete proposal to raw with given limit bigger than Size", func() { 115 | envs := make(chan *basic.TracingEnvelope, 1002) 116 | defer close(envs) 117 | errorCh := make(chan error, 1002) 118 | defer close(errorCh) 119 | config, err := basic.LoadConfig(configFile.Name()) 120 | Expect(err).NotTo(HaveOccurred()) 121 | crypto, err := config.LoadCrypto() 122 | Expect(err).NotTo(HaveOccurred()) 123 | t := time.Now() 124 | fackEnvelopGenerator := &trafficGenerator.FackEnvelopGenerator{ 125 | Num: 12, 126 | Burst: 10, 127 | R: 0, 128 | Config: config, 129 | Crypto: crypto, 130 | Envs: envs, 131 | ErrorCh: errorCh, 132 | } 133 | fackEnvelopGenerator.Start() 134 | t1 := time.Now() 135 | Expect(envs).To(HaveLen(12)) 136 | Expect(t1.Sub(t)).To(BeNumerically("<", 2*time.Second)) 137 | }) 138 | 139 | }) 140 | -------------------------------------------------------------------------------- /pkg/infra/basic/config_test.go: -------------------------------------------------------------------------------- 1 | package basic_test 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | type files struct { 13 | TlsFile string 14 | PolicyFile string 15 | } 16 | 17 | func generateConfigFile(FileName string, values interface{}) { 18 | var Text = `# Definition of nodes 19 | org1peer0: &org1peer0 20 | addr: localhost:7051 21 | ssl_target_name_override: peer0.org1.example.com 22 | tls_ca_cert: {{.TlsFile}} 23 | org: org1 24 | org2peer0: &org2peer0 25 | addr: peer0.org2.example.com:7051 26 | tls_ca_cert: {{.TlsFile}} 27 | org: org2 28 | org0orderer0: &org0orderer0 29 | addr: orderer.example.com:7050 30 | tls_ca_cert: {{.TlsFile}} 31 | org: org0 32 | 33 | endorsers: 34 | - *org1peer0 35 | - *org2peer0 36 | committers: 37 | - *org2peer0 38 | commitThreshold: 1 39 | orderer: *org0orderer0 40 | policyFile: {{.PolicyFile}} 41 | 42 | channel: mychannel 43 | chaincode: mycc 44 | args: 45 | - invoke 46 | - a 47 | - b 48 | - 1 49 | mspid: Org1MSP 50 | private_key: /path/to/private.key 51 | sign_cert: /path/to/sign.cert 52 | num_of_conn: 20 53 | client_per_conn: 40 54 | ` 55 | tmpl, err := template.New("test").Parse(Text) 56 | if err != nil { 57 | panic(err) 58 | } 59 | file, err := os.OpenFile(FileName, os.O_CREATE|os.O_WRONLY, 0755) 60 | if err != nil { 61 | panic(err) 62 | } 63 | defer file.Close() 64 | err = tmpl.Execute(file, values) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | 70 | var _ = Describe("Config", func() { 71 | 72 | Context("good", func() { 73 | It("successful loads", func() { 74 | tlsFile, err := os.CreateTemp("", "dummy-*.pem") 75 | Expect(err).NotTo(HaveOccurred()) 76 | defer os.Remove(tlsFile.Name()) 77 | 78 | _, err = tlsFile.Write([]byte("a")) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | policyFile, err := os.CreateTemp("", "dummy-*.pem") 82 | Expect(err).NotTo(HaveOccurred()) 83 | defer os.Remove(policyFile.Name()) 84 | 85 | _, err = policyFile.Write([]byte("b")) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | f, _ := os.CreateTemp("", "config-*.yaml") 89 | defer os.Remove(f.Name()) 90 | 91 | file := files{TlsFile: tlsFile.Name(), 92 | PolicyFile: policyFile.Name()} 93 | 94 | generateConfigFile(f.Name(), file) 95 | 96 | c, err := basic.LoadConfig(f.Name()) 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(c).To(Equal(basic.Config{ 99 | Endorsers: []basic.Node{ 100 | {Addr: "localhost:7051", SslTargetNameOverride: "peer0.org1.example.com", TLSCACert: tlsFile.Name(), TLSCACertByte: []byte("a"), Org: "org1"}, 101 | {Addr: "peer0.org2.example.com:7051", TLSCACert: tlsFile.Name(), TLSCACertByte: []byte("a"), Org: "org2"}, 102 | }, 103 | Committers: []basic.Node{{Addr: "peer0.org2.example.com:7051", TLSCACert: tlsFile.Name(), TLSCACertByte: []byte("a"), Org: "org2"}}, 104 | CommitThreshold: 1, 105 | Orderer: basic.Node{Addr: "orderer.example.com:7050", TLSCACert: tlsFile.Name(), TLSCACertByte: []byte("a"), Org: "org0"}, 106 | PolicyFile: policyFile.Name(), 107 | Rule: "b", 108 | Channel: "mychannel", 109 | Chaincode: "mycc", 110 | Version: "", 111 | Args: []string{"invoke", "a", "b", "1"}, 112 | MSPID: "Org1MSP", 113 | PrivateKey: "/path/to/private.key", 114 | SignCert: "/path/to/sign.cert", 115 | NumOfConn: 20, 116 | ClientPerConn: 40, 117 | })) 118 | _, err = c.LoadCrypto() 119 | Expect(err).Should(MatchError(ContainSubstring("error loading priv key"))) 120 | }) 121 | }) 122 | 123 | Context("bad", func() { 124 | It("fails to load missing config file", func() { 125 | _, err := basic.LoadConfig("invalid_file") 126 | Expect(err).Should(MatchError(ContainSubstring("invalid_file"))) 127 | }) 128 | 129 | It("fails to load invalid config file", func() { 130 | 131 | f, _ := os.CreateTemp("", "config-*.yaml") 132 | defer os.Remove(f.Name()) 133 | 134 | file := files{TlsFile: "invalid_file", 135 | PolicyFile: "invalid_file"} 136 | 137 | generateConfigFile(f.Name(), file) 138 | 139 | _, err := basic.LoadConfig(f.Name()) 140 | Expect(err).Should(MatchError(ContainSubstring("invalid_file"))) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /pkg/infra/trafficGenerator/initiator_test.go: -------------------------------------------------------------------------------- 1 | package trafficGenerator_test 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/hyperledger-twgc/tape/e2e" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 9 | "github.com/hyperledger-twgc/tape/pkg/infra/trafficGenerator" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var _ = Describe("Initiator", func() { 17 | 18 | var ( 19 | configFile *os.File 20 | tmpDir string 21 | logger = log.New() 22 | ) 23 | 24 | BeforeEach(func() { 25 | var err error 26 | tmpDir, err = os.MkdirTemp("", "tape-") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | mtlsCertFile, err := os.CreateTemp(tmpDir, "mtls-*.crt") 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | mtlsKeyFile, err := os.CreateTemp(tmpDir, "mtls-*.key") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | PolicyFile, err := os.CreateTemp(tmpDir, "policy") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | err = e2e.GeneratePolicy(PolicyFile) 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | err = e2e.GenerateCertAndKeys(mtlsKeyFile, mtlsCertFile) 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | mtlsCertFile.Close() 45 | mtlsKeyFile.Close() 46 | PolicyFile.Close() 47 | 48 | configFile, err = os.CreateTemp(tmpDir, "config*.yaml") 49 | Expect(err).NotTo(HaveOccurred()) 50 | configValue := e2e.Values{ 51 | PrivSk: mtlsKeyFile.Name(), 52 | SignCert: mtlsCertFile.Name(), 53 | Mtls: false, 54 | PeersAddrs: []string{"dummy"}, 55 | OrdererAddr: "dummy", 56 | CommitThreshold: 1, 57 | PolicyFile: PolicyFile.Name(), 58 | } 59 | e2e.GenerateConfigFile(configFile.Name(), configValue) 60 | }) 61 | 62 | AfterEach(func() { 63 | os.RemoveAll(tmpDir) 64 | }) 65 | 66 | PIt("should crete proposal to raw without limit when number is 0", func() { 67 | raw := make(chan *basic.TracingProposal, 1002) 68 | //defer close(raw) 69 | errorCh := make(chan error, 1002) 70 | defer close(errorCh) 71 | config, err := basic.LoadConfig(configFile.Name()) 72 | Expect(err).NotTo(HaveOccurred()) 73 | crypto, err := config.LoadCrypto() 74 | Expect(err).NotTo(HaveOccurred()) 75 | Initiator := &trafficGenerator.Initiator{0, 10, 0, config, crypto, logger, raw, errorCh} 76 | go Initiator.Start() 77 | for i := 0; i < 1002; i++ { 78 | _, flag := <-raw 79 | Expect(flag).To(BeFalse()) 80 | } 81 | close(raw) 82 | }) 83 | 84 | It("should crete proposal to raw without limit when limit is 0", func() { 85 | raw := make(chan *basic.TracingProposal, 1002) 86 | defer close(raw) 87 | errorCh := make(chan error, 1002) 88 | defer close(errorCh) 89 | config, err := basic.LoadConfig(configFile.Name()) 90 | Expect(err).NotTo(HaveOccurred()) 91 | crypto, err := config.LoadCrypto() 92 | Expect(err).NotTo(HaveOccurred()) 93 | t := time.Now() 94 | Initiator := &trafficGenerator.Initiator{1002, 10, 0, config, crypto, logger, raw, errorCh} 95 | Initiator.Start() 96 | t1 := time.Now() 97 | Expect(raw).To(HaveLen(1002)) 98 | Expect(t1.Sub(t)).To(BeNumerically("<", 2*time.Second)) 99 | }) 100 | 101 | It("should crete proposal to raw with given limit bigger than 0 less than size", func() { 102 | raw := make(chan *basic.TracingProposal, 1002) 103 | defer close(raw) 104 | errorCh := make(chan error, 1002) 105 | defer close(errorCh) 106 | config, err := basic.LoadConfig(configFile.Name()) 107 | Expect(err).NotTo(HaveOccurred()) 108 | crypto, err := config.LoadCrypto() 109 | Expect(err).NotTo(HaveOccurred()) 110 | t := time.Now() 111 | Initiator := &trafficGenerator.Initiator{12, 10, 1, config, crypto, logger, raw, errorCh} 112 | Initiator.Start() 113 | t1 := time.Now() 114 | Expect(raw).To(HaveLen(12)) 115 | Expect(t1.Sub(t)).To(BeNumerically(">", 2*time.Second)) 116 | }) 117 | 118 | It("should crete proposal to raw with given limit bigger than Size", func() { 119 | raw := make(chan *basic.TracingProposal, 1002) 120 | defer close(raw) 121 | errorCh := make(chan error, 1002) 122 | defer close(errorCh) 123 | config, err := basic.LoadConfig(configFile.Name()) 124 | Expect(err).NotTo(HaveOccurred()) 125 | crypto, err := config.LoadCrypto() 126 | Expect(err).NotTo(HaveOccurred()) 127 | t := time.Now() 128 | Initiator := &trafficGenerator.Initiator{12, 10, 0, config, crypto, logger, raw, errorCh} 129 | Initiator.Start() 130 | t1 := time.Now() 131 | Expect(raw).To(HaveLen(12)) 132 | Expect(t1.Sub(t)).To(BeNumerically("<", 2*time.Second)) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /internal/fabric/core/comm/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package comm 8 | 9 | import ( 10 | "crypto/x509" 11 | "time" 12 | 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | // Configuration defaults 17 | var ( 18 | // Max send and receive bytes for grpc clients and servers 19 | MaxRecvMsgSize = 100 * 1024 * 1024 20 | MaxSendMsgSize = 100 * 1024 * 1024 21 | // Default peer keepalive options 22 | DefaultKeepaliveOptions = KeepaliveOptions{ 23 | ClientInterval: time.Duration(1) * time.Minute, // 1 min 24 | ClientTimeout: time.Duration(20) * time.Second, // 20 sec - gRPC default 25 | ServerInterval: time.Duration(2) * time.Hour, // 2 hours - gRPC default 26 | ServerTimeout: time.Duration(20) * time.Second, // 20 sec - gRPC default 27 | ServerMinInterval: time.Duration(1) * time.Minute, // match ClientInterval 28 | } 29 | ) 30 | 31 | // ServerConfig defines the parameters for configuring a GRPCServer instance 32 | type ServerConfig struct { 33 | // ConnectionTimeout specifies the timeout for connection establishment 34 | // for all new connections 35 | ConnectionTimeout time.Duration 36 | // SecOpts defines the security parameters 37 | SecOpts SecureOptions 38 | // KaOpts defines the keepalive parameters 39 | KaOpts KeepaliveOptions 40 | // StreamInterceptors specifies a list of interceptors to apply to 41 | // streaming RPCs. They are executed in order. 42 | StreamInterceptors []grpc.StreamServerInterceptor 43 | // UnaryInterceptors specifies a list of interceptors to apply to unary 44 | // RPCs. They are executed in order. 45 | UnaryInterceptors []grpc.UnaryServerInterceptor 46 | // HealthCheckEnabled enables the gRPC Health Checking Protocol for the server 47 | HealthCheckEnabled bool 48 | } 49 | 50 | // ClientConfig defines the parameters for configuring a GRPCClient instance 51 | type ClientConfig struct { 52 | // SecOpts defines the security parameters 53 | SecOpts SecureOptions 54 | // KaOpts defines the keepalive parameters 55 | KaOpts KeepaliveOptions 56 | // Timeout specifies how long the client will block when attempting to 57 | // establish a connection 58 | Timeout time.Duration 59 | // AsyncConnect makes connection creation non blocking 60 | AsyncConnect bool 61 | } 62 | 63 | // Clone clones this ClientConfig 64 | func (cc ClientConfig) Clone() ClientConfig { 65 | shallowClone := cc 66 | return shallowClone 67 | } 68 | 69 | // SecureOptions defines the security parameters (e.g. TLS) for a 70 | // GRPCServer or GRPCClient instance 71 | type SecureOptions struct { 72 | // VerifyCertificate, if not nil, is called after normal 73 | // certificate verification by either a TLS client or server. 74 | // If it returns a non-nil error, the handshake is aborted and that error results. 75 | VerifyCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error 76 | // PEM-encoded X509 public key to be used for TLS communication 77 | Certificate []byte 78 | // PEM-encoded private key to be used for TLS communication 79 | Key []byte 80 | // Set of PEM-encoded X509 certificate authorities used by clients to 81 | // verify server certificates 82 | ServerRootCAs [][]byte 83 | // Set of PEM-encoded X509 certificate authorities used by servers to 84 | // verify client certificates 85 | ClientRootCAs [][]byte 86 | // Whether or not to use TLS for communication 87 | UseTLS bool 88 | // Whether or not TLS client must present certificates for authentication 89 | RequireClientCert bool 90 | // CipherSuites is a list of supported cipher suites for TLS 91 | CipherSuites []uint16 92 | // TimeShift makes TLS handshakes time sampling shift to the past by a given duration 93 | TimeShift time.Duration 94 | } 95 | 96 | // KeepaliveOptions is used to set the gRPC keepalive settings for both 97 | // clients and servers 98 | type KeepaliveOptions struct { 99 | // ClientInterval is the duration after which if the client does not see 100 | // any activity from the server it pings the server to see if it is alive 101 | ClientInterval time.Duration 102 | // ClientTimeout is the duration the client waits for a response 103 | // from the server after sending a ping before closing the connection 104 | ClientTimeout time.Duration 105 | // ServerInterval is the duration after which if the server does not see 106 | // any activity from the client it pings the client to see if it is alive 107 | ServerInterval time.Duration 108 | // ServerTimeout is the duration the server waits for a response 109 | // from the client after sending a ping before closing the connection 110 | ServerTimeout time.Duration 111 | // ServerMinInterval is the minimum permitted time between client pings. 112 | // If clients send pings more frequently, the server will disconnect them 113 | ServerMinInterval time.Duration 114 | } 115 | -------------------------------------------------------------------------------- /pkg/infra/observer/commitObserver.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sync" 7 | "time" 8 | 9 | "github.com/opentracing/opentracing-go" 10 | 11 | "github.com/hyperledger-twgc/tape/internal/fabric/protoutil" 12 | "github.com/hyperledger-twgc/tape/pkg/infra" 13 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 14 | 15 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 16 | "github.com/hyperledger/fabric-protos-go-apiv2/orderer" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type CommitObserver struct { 21 | d orderer.AtomicBroadcast_DeliverClient 22 | n int 23 | logger *log.Logger 24 | Now time.Time 25 | errorCh chan error 26 | finishCh chan struct{} 27 | once *sync.Once 28 | addresses []string 29 | finishflag bool 30 | } 31 | 32 | func CreateCommitObserver( 33 | channel string, 34 | node basic.Node, 35 | crypto *basic.CryptoImpl, 36 | logger *log.Logger, 37 | n int, 38 | config basic.Config, 39 | errorCh chan error, 40 | finishCh chan struct{}, 41 | once *sync.Once, 42 | finishflag bool) (*CommitObserver, error) { 43 | if len(node.Addr) == 0 { 44 | return nil, nil 45 | } 46 | deliverer, err := basic.CreateDeliverClient(node) 47 | if err != nil { 48 | return nil, err 49 | } 50 | seek, err := CreateSignedDeliverNewestEnv(channel, crypto) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if err = deliverer.Send(seek); err != nil { 55 | return nil, err 56 | } 57 | // drain first response 58 | _, err = deliverer.Recv() 59 | if err != nil { 60 | return nil, err 61 | } 62 | addresses := make([]string, 0) 63 | for _, v := range config.Committers { 64 | addresses = append(addresses, v.Addr) 65 | } 66 | return &CommitObserver{d: deliverer, 67 | n: n, 68 | logger: logger, 69 | errorCh: errorCh, 70 | finishCh: finishCh, 71 | addresses: addresses, 72 | once: once, 73 | finishflag: finishflag, 74 | }, nil 75 | } 76 | 77 | func (o *CommitObserver) Start() { 78 | o.Now = time.Now() 79 | 80 | o.logger.Debugf("start observer for orderer") 81 | n := 0 82 | for { 83 | r, err := o.d.Recv() 84 | if err != nil { 85 | o.errorCh <- err 86 | } 87 | if r == nil { 88 | panic("Received nil message, but expect a valid block instead. You could look into your peer logs for more info") 89 | } 90 | block := r.GetBlock() 91 | tx := len(block.Data.Data) 92 | n += tx 93 | fmt.Printf("From Orderer Time %8.2fs\tBlock %6d\t Tx %6d\n", time.Since(o.Now).Seconds(), block.Header.Number, tx) 94 | for _, data := range block.Data.Data { 95 | txID := "" 96 | env, err := protoutil.GetEnvelopeFromBlock(data) 97 | if err != nil { 98 | continue 99 | } 100 | payload, err := protoutil.UnmarshalPayload(env.Payload) 101 | if err != nil { 102 | continue 103 | } 104 | chdr, err := protoutil.UnmarshalChannelHeader(payload.Header.ChannelHeader) 105 | if err != nil { 106 | continue 107 | } 108 | if common.HeaderType(chdr.Type) == common.HeaderType_ENDORSER_TRANSACTION { 109 | txID = chdr.TxId 110 | } 111 | if txID != "" { 112 | tapeSpan := basic.GetGlobalSpan() 113 | tapeSpan.FinishWithMap(txID, "", basic.CONSESUS) 114 | var span opentracing.Span 115 | if basic.GetMod() == infra.FULLPROCESS { 116 | Global_Span := tapeSpan.GetSpan(txID, "", basic.TRANSACTION) 117 | span = tapeSpan.SpanIntoMap(txID, "", basic.COMMIT_AT_ALL_PEERS, Global_Span) 118 | } else { 119 | span = tapeSpan.SpanIntoMap(txID, "", basic.COMMIT_AT_ALL_PEERS, nil) 120 | } 121 | tapeSpan.SpanIntoMap(txID, "", basic.COMMIT_AT_NETWORK, span) 122 | if basic.GetMod() != infra.COMMIT { 123 | for _, v := range o.addresses { 124 | tapeSpan.SpanIntoMap(txID, v, basic.COMMIT_AT_PEER, span) 125 | } 126 | } 127 | basic.LogEvent(o.logger, txID, "BlockFromOrderer") 128 | } 129 | } 130 | if o.n > 0 && o.finishflag { 131 | if n >= o.n { 132 | // consider with multiple threads need close this channel, need a once here to avoid channel been closed in multiple times 133 | o.once.Do(func() { 134 | close(o.finishCh) 135 | }) 136 | return 137 | } 138 | } 139 | } 140 | } 141 | 142 | func (o *CommitObserver) GetTime() time.Time { 143 | return o.Now 144 | } 145 | 146 | func CreateSignedDeliverNewestEnv(ch string, signer *basic.CryptoImpl) (*common.Envelope, error) { 147 | start := &orderer.SeekPosition{ 148 | Type: &orderer.SeekPosition_Newest{ 149 | Newest: &orderer.SeekNewest{}, 150 | }, 151 | } 152 | 153 | stop := &orderer.SeekPosition{ 154 | Type: &orderer.SeekPosition_Specified{ 155 | Specified: &orderer.SeekSpecified{ 156 | Number: math.MaxUint64, 157 | }, 158 | }, 159 | } 160 | 161 | seekInfo := &orderer.SeekInfo{ 162 | Start: start, 163 | Stop: stop, 164 | Behavior: orderer.SeekInfo_BLOCK_UNTIL_READY, 165 | } 166 | 167 | return protoutil.CreateSignedEnvelope( 168 | common.HeaderType_DELIVER_SEEK_INFO, 169 | ch, 170 | signer, 171 | seekInfo, 172 | 0, 173 | 0, 174 | ) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/infra/observer/block_collector.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/hyperledger-twgc/tape/pkg/infra" 10 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 11 | "github.com/hyperledger-twgc/tape/pkg/infra/bitmap" 12 | 13 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 14 | "github.com/pkg/errors" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // BlockCollector keeps track of committed blocks on multiple peers. 19 | // This is used when a block is considered confirmed only when committed 20 | // on a certain number of peers within network. 21 | type BlockCollector struct { 22 | sync.Mutex 23 | thresholdP, totalP, totalTx int 24 | registry map[uint64]*bitmap.BitMap 25 | ctx context.Context 26 | blockCh chan *AddressedBlock 27 | finishCh chan struct{} 28 | logger *log.Logger 29 | once *sync.Once 30 | printResult bool // controls whether to print block commit message. Tests set this to false to avoid polluting stdout. 31 | finishflag bool 32 | } 33 | 34 | // AddressedBlock describe the source of block 35 | type AddressedBlock struct { 36 | *peer.FilteredBlock 37 | Address int // source peer's number 38 | Now time.Duration 39 | } 40 | 41 | // NewBlockCollector creates a BlockCollector 42 | func NewBlockCollector(threshold int, totalP int, 43 | ctx context.Context, 44 | blockCh chan *AddressedBlock, 45 | finishCh chan struct{}, 46 | totalTx int, 47 | printResult bool, 48 | logger *log.Logger, 49 | once *sync.Once, finishflag bool) (*BlockCollector, error) { 50 | registry := make(map[uint64]*bitmap.BitMap) 51 | if threshold <= 0 || totalP <= 0 { 52 | return nil, errors.New("threshold and total must be greater than zero") 53 | } 54 | if threshold > totalP { 55 | return nil, errors.Errorf("threshold [%d] must be less than or equal to total [%d]", threshold, totalP) 56 | } 57 | return &BlockCollector{ 58 | thresholdP: threshold, 59 | totalP: totalP, 60 | totalTx: totalTx, 61 | registry: registry, 62 | ctx: ctx, 63 | blockCh: blockCh, 64 | finishCh: finishCh, 65 | printResult: printResult, 66 | logger: logger, 67 | once: once, 68 | finishflag: finishflag, 69 | }, nil 70 | } 71 | 72 | func (bc *BlockCollector) Start() { 73 | for { 74 | select { 75 | case block := <-bc.blockCh: 76 | bc.commit(block) 77 | case <-bc.ctx.Done(): 78 | return 79 | } 80 | } 81 | } 82 | 83 | // TODO This function contains too many functions and needs further optimization 84 | // commit commits a block to collector. 85 | // If the number of peers on which this block has been committed has satisfied thresholdP, 86 | // adds the number to the totalTx. 87 | func (bc *BlockCollector) commit(block *AddressedBlock) { 88 | breakbynumber := true 89 | if bc.totalTx <= 0 { 90 | breakbynumber = false 91 | } 92 | bitMap, ok := bc.registry[block.Number] 93 | if !ok { 94 | // The block with Number is received for the first time 95 | b, err := bitmap.NewBitMap(bc.totalP) 96 | if err != nil { 97 | panic("Can not make new bitmap for BlockCollector" + err.Error()) 98 | } 99 | bc.registry[block.Number] = &b 100 | bitMap = &b 101 | } 102 | // When the block from Address has been received before, return directly. 103 | if bitMap.Has(block.Address) { 104 | return 105 | } 106 | 107 | bitMap.Set(block.Address) 108 | cnt := bitMap.Count() 109 | 110 | // newly committed block just hits threshold 111 | if cnt == bc.thresholdP { 112 | if bc.printResult { 113 | // todo: logging 114 | // receive tx over threshold 115 | fmt.Printf("Time %8.2fs\tBlock %6d\tTx %6d\t \n", block.Now.Seconds(), block.Number, len(block.FilteredTransactions)) 116 | for _, b := range block.FilteredBlock.FilteredTransactions { 117 | basic.LogEvent(bc.logger, b.Txid, "CommitAtPeersOverThreshold") 118 | tapeSpan := basic.GetGlobalSpan() 119 | tapeSpan.FinishWithMap(b.Txid, "", basic.COMMIT_AT_NETWORK) 120 | // if prometheus 121 | // report transaction readlatency with peer in label 122 | basic.GetLatencyMap().TransactionLatency(b.Txid) 123 | } 124 | } 125 | if breakbynumber { 126 | bc.totalTx -= len(block.FilteredTransactions) 127 | if bc.totalTx <= 0 && bc.finishflag { 128 | // consider with multiple threads need close this channel, need a once here to avoid channel been closed in multiple times 129 | bc.once.Do(func() { 130 | close(bc.finishCh) 131 | }) 132 | } 133 | } 134 | } 135 | 136 | // TODO issue176 137 | if cnt == bc.totalP { 138 | // committed on all peers, remove from registry 139 | // todo: logging 140 | // end of from peers 141 | // end of transaction creation 142 | delete(bc.registry, block.Number) 143 | for _, b := range block.FilteredBlock.FilteredTransactions { 144 | basic.LogEvent(bc.logger, b.Txid, "CommitAtPeers") 145 | tapeSpan := basic.GetGlobalSpan() 146 | tapeSpan.FinishWithMap(b.Txid, "", basic.COMMIT_AT_ALL_PEERS) 147 | if basic.GetMod() == infra.FULLPROCESS { 148 | tapeSpan.FinishWithMap(b.Txid, "", basic.TRANSACTION) 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/fabric/core/comm/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright IBM Corp. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package comm 8 | 9 | import ( 10 | "crypto/tls" 11 | "crypto/x509" 12 | "time" 13 | 14 | grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" 15 | 16 | opentracing "github.com/opentracing/opentracing-go" 17 | "github.com/pkg/errors" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/credentials/insecure" 20 | "google.golang.org/grpc/keepalive" 21 | ) 22 | 23 | type GRPCClient struct { 24 | // TLS configuration used by the grpc.ClientConn 25 | tlsConfig *tls.Config 26 | // Options for setting up new connections 27 | dialOpts []grpc.DialOption 28 | // Maximum message size the client can receive 29 | maxRecvMsgSize int 30 | // Maximum message size the client can send 31 | maxSendMsgSize int 32 | } 33 | 34 | // NewGRPCClient creates a new implementation of GRPCClient given an address 35 | // and client configuration 36 | func NewGRPCClient(config ClientConfig) (*GRPCClient, error) { 37 | client := &GRPCClient{} 38 | 39 | // parse secure options 40 | err := client.parseSecureOptions(config.SecOpts) 41 | if err != nil { 42 | return client, err 43 | } 44 | 45 | // keepalive options 46 | 47 | kap := keepalive.ClientParameters{ 48 | Time: config.KaOpts.ClientInterval, 49 | PermitWithoutStream: true, 50 | } 51 | // set keepalive 52 | client.dialOpts = append(client.dialOpts, grpc.WithKeepaliveParams(kap)) 53 | // Unless asynchronous connect is set, make connection establishment blocking. 54 | /*if !config.AsyncConnect { 55 | client.dialOpts = append(client.dialOpts, grpc.WithBlock()) 56 | client.dialOpts = append(client.dialOpts, grpc.FailOnNonTempDialError(true)) 57 | }*/ 58 | // set send/recv message size to package defaults 59 | client.maxRecvMsgSize = MaxRecvMsgSize 60 | client.maxSendMsgSize = MaxSendMsgSize 61 | 62 | return client, nil 63 | } 64 | 65 | func (client *GRPCClient) parseSecureOptions(opts SecureOptions) error { 66 | // if TLS is not enabled, return 67 | if !opts.UseTLS { 68 | return nil 69 | } 70 | 71 | client.tlsConfig = &tls.Config{ 72 | VerifyPeerCertificate: opts.VerifyCertificate, 73 | MinVersion: tls.VersionTLS12} // TLS 1.2 only 74 | if len(opts.ServerRootCAs) > 0 { 75 | client.tlsConfig.RootCAs = x509.NewCertPool() 76 | for _, certBytes := range opts.ServerRootCAs { 77 | err := AddPemToCertPool(certBytes, client.tlsConfig.RootCAs) 78 | if err != nil { 79 | //commLogger.Debugf("error adding root certificate: %v", err) 80 | return errors.WithMessage(err, 81 | "error adding root certificate") 82 | } 83 | } 84 | } 85 | if opts.RequireClientCert { 86 | // make sure we have both Key and Certificate 87 | if opts.Key != nil && 88 | opts.Certificate != nil { 89 | cert, err := tls.X509KeyPair(opts.Certificate, 90 | opts.Key) 91 | if err != nil { 92 | return errors.WithMessage(err, "failed to "+ 93 | "load client certificate") 94 | } 95 | client.tlsConfig.Certificates = append( 96 | client.tlsConfig.Certificates, cert) 97 | } else { 98 | return errors.New("both Key and Certificate " + 99 | "are required when using mutual TLS") 100 | } 101 | } 102 | 103 | if opts.TimeShift > 0 { 104 | client.tlsConfig.Time = func() time.Time { 105 | return time.Now().Add((-1) * opts.TimeShift) 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | type TLSOption func(tlsConfig *tls.Config) 113 | 114 | // NewConnection returns a grpc.ClientConn for the target address and 115 | // overrides the server name used to verify the hostname on the 116 | // certificate returned by a server when using TLS 117 | func (client *GRPCClient) NewConnection(address string, tlsOptions ...TLSOption) (*grpc.ClientConn, error) { 118 | var dialOpts []grpc.DialOption 119 | dialOpts = append(dialOpts, client.dialOpts...) 120 | 121 | // set transport credentials and max send/recv message sizes 122 | // immediately before creating a connection in order to allow 123 | // SetServerRootCAs / SetMaxRecvMsgSize / SetMaxSendMsgSize 124 | // to take effect on a per connection basis 125 | if client.tlsConfig != nil { 126 | dialOpts = append(dialOpts, grpc.WithTransportCredentials( 127 | &DynamicClientCredentials{ 128 | TLSConfig: client.tlsConfig, 129 | TLSOptions: tlsOptions, 130 | }, 131 | )) 132 | } else { 133 | dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) 134 | } 135 | 136 | dialOpts = append(dialOpts, grpc.WithDefaultCallOptions( 137 | grpc.MaxCallRecvMsgSize(client.maxRecvMsgSize), 138 | grpc.MaxCallSendMsgSize(client.maxSendMsgSize), 139 | )) 140 | 141 | tracer := opentracing.GlobalTracer() 142 | opts := []grpc_opentracing.Option{ 143 | grpc_opentracing.WithTracer(tracer), 144 | } 145 | 146 | dialOpts = append(dialOpts, 147 | grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(opts...)), 148 | grpc.WithStreamInterceptor(grpc_opentracing.StreamClientInterceptor(opts...)), 149 | ) 150 | 151 | conn, err := grpc.NewClient(address, dialOpts...) 152 | if err != nil { 153 | return nil, errors.WithMessage(errors.WithStack(err), 154 | "failed to create new connection") 155 | } 156 | return conn, nil 157 | } 158 | -------------------------------------------------------------------------------- /pkg/infra/basic/logger.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "time" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/prometheus/client_golang/prometheus" 10 | log "github.com/sirupsen/logrus" 11 | jaeger "github.com/uber/jaeger-client-go" 12 | "github.com/uber/jaeger-client-go/config" 13 | ) 14 | 15 | func LogEvent(logger *log.Logger, txid, event string) { 16 | now := time.Now() 17 | time_str := now.Format(time.RFC3339Nano) 18 | logger.Debugf("For txid %s, event %s at %s", txid, event, time_str) 19 | } 20 | 21 | // Init returns an instance of Jaeger Tracer that samples 100% 22 | // of traces and logs all spans to stdout. 23 | func Init(service string) (opentracing.Tracer, io.Closer) { 24 | cfg := &config.Configuration{ 25 | Sampler: &config.SamplerConfig{ 26 | Type: "const", 27 | Param: 1, 28 | }, 29 | Reporter: &config.ReporterConfig{ 30 | LogSpans: false, 31 | }, 32 | } 33 | cfg.ServiceName = service 34 | tracer, closer, err := cfg.NewTracer( 35 | config.Logger(jaeger.StdLogger), 36 | ) 37 | if err != nil { 38 | log.Fatalf("ERROR: cannot init Jaeger: %v", err) 39 | } 40 | return tracer, closer 41 | } 42 | 43 | const ( 44 | TRANSACTION = "TRANSACTION" 45 | TRANSACTIONSTART = "TRANSACTIONSTART" 46 | SIGN_PROPOSAL = "SIGN_PROPOSAL" 47 | ENDORSEMENT = "ENDORSEMENT" 48 | ENDORSEMENT_AT_PEER = "ENDORSEMENT_AT_PEER" 49 | COLLECT_ENDORSEMENT = "COLLECT_ENDORSEMENT" 50 | SIGN_ENVELOP = "SIGN_ENVELOP" 51 | BROADCAST = "BROADCAST" 52 | CONSESUS = "CONSESUS" 53 | COMMIT_AT_NETWORK = "COMMIT_AT_NETWORK" 54 | COMMIT_AT_PEER = "COMMIT_AT_PEER" 55 | COMMIT_AT_ALL_PEERS = "COMMIT_AT_ALL_PEERS" 56 | ) 57 | 58 | var TapeSpan *TracingSpans 59 | var LatencyM *LatencyMap 60 | var ProcessMod int 61 | var onceSpan sync.Once 62 | 63 | type LatencyMap struct { 64 | Map map[string]time.Time 65 | Lock sync.Mutex 66 | Mod int 67 | Transactionlatency, Readlatency *prometheus.SummaryVec 68 | Enable bool 69 | } 70 | 71 | type TracingSpans struct { 72 | Spans map[string]opentracing.Span 73 | Lock sync.Mutex 74 | } 75 | 76 | func (TS *TracingSpans) MakeSpan(txid, address, event string, parent opentracing.Span) opentracing.Span { 77 | str := event + address 78 | if parent == nil { 79 | return opentracing.GlobalTracer().StartSpan(str, opentracing.Tag{Key: "txid", Value: txid}) 80 | } else { 81 | return opentracing.GlobalTracer().StartSpan(str, opentracing.ChildOf(parent.Context()), opentracing.Tag{Key: "txid", Value: txid}) 82 | } 83 | } 84 | 85 | func (TS *TracingSpans) GetSpan(txid, address, event string) opentracing.Span { 86 | TS.Lock.Lock() 87 | defer TS.Lock.Unlock() 88 | 89 | str := event + txid + address 90 | span, ok := TS.Spans[str] 91 | if ok { 92 | return span 93 | } 94 | return nil 95 | } 96 | 97 | func (TS *TracingSpans) SpanIntoMap(txid, address, event string, parent opentracing.Span) opentracing.Span { 98 | TS.Lock.Lock() 99 | defer TS.Lock.Unlock() 100 | 101 | str := event + txid + address 102 | span, ok := TS.Spans[str] 103 | if !ok { 104 | span = TS.MakeSpan(txid, address, event, parent) 105 | TS.Spans[str] = span 106 | } 107 | return span 108 | } 109 | 110 | func (TS *TracingSpans) FinishWithMap(txid, address, event string) { 111 | TS.Lock.Lock() 112 | defer TS.Lock.Unlock() 113 | 114 | str := event + txid + address 115 | span, ok := TS.Spans[str] 116 | if ok { 117 | span.Finish() 118 | delete(TS.Spans, str) 119 | } 120 | } 121 | 122 | func GetGlobalSpan() *TracingSpans { 123 | onceSpan.Do(func() { 124 | Spans := make(map[string]opentracing.Span) 125 | 126 | TapeSpan = &TracingSpans{ 127 | Spans: Spans, 128 | } 129 | }) 130 | 131 | return TapeSpan 132 | } 133 | 134 | func SetMod(mod int) { 135 | ProcessMod = mod 136 | } 137 | 138 | func GetMod() int { 139 | return ProcessMod 140 | } 141 | 142 | func InitLatencyMap(Transactionlatency, Readlatency *prometheus.SummaryVec, Mod int, Enable bool) *LatencyMap { 143 | Map := make(map[string]time.Time) 144 | 145 | LatencyM = &LatencyMap{ 146 | Map: Map, 147 | Mod: Mod, 148 | Transactionlatency: Transactionlatency, 149 | Readlatency: Readlatency, 150 | Enable: Enable, 151 | } 152 | 153 | return GetLatencyMap() 154 | } 155 | 156 | func GetLatencyMap() *LatencyMap { 157 | return LatencyM 158 | } 159 | 160 | func (LM *LatencyMap) StartTracing(txid string) { 161 | if LM == nil { 162 | return 163 | } 164 | LM.Lock.Lock() 165 | defer LM.Lock.Unlock() 166 | 167 | if LM.Enable { 168 | LM.Map[txid] = time.Now() 169 | } 170 | } 171 | 172 | func (LM *LatencyMap) ReportReadLatency(txid, label string) { 173 | if LM == nil { 174 | return 175 | } 176 | LM.Lock.Lock() 177 | defer LM.Lock.Unlock() 178 | 179 | start_time, ok := LM.Map[txid] 180 | if ok && LM.Readlatency != nil { 181 | diff := time.Since(start_time) 182 | LM.Readlatency.WithLabelValues(label).Observe(diff.Seconds()) 183 | if LM.Mod == 4 || LM.Mod == 7 { 184 | delete(LM.Map, txid) 185 | } 186 | } 187 | } 188 | 189 | func (LM *LatencyMap) TransactionLatency(txid string) { 190 | if LM == nil { 191 | return 192 | } 193 | LM.Lock.Lock() 194 | defer LM.Lock.Unlock() 195 | 196 | start_time, ok := LM.Map[txid] 197 | if ok && LM.Transactionlatency != nil { 198 | diff := time.Since(start_time) 199 | LM.Transactionlatency.WithLabelValues("CommitAtPeersOverThreshold").Observe(diff.Seconds()) 200 | delete(LM.Map, txid) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pkg/infra/basic/config.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "os" 8 | "sync" 9 | 10 | "github.com/opentracing/opentracing-go" 11 | 12 | "github.com/hyperledger-twgc/tape/internal/fabric/bccsp/utils" 13 | 14 | "github.com/gogo/protobuf/proto" 15 | "github.com/hyperledger/fabric-protos-go-apiv2/common" 16 | "github.com/hyperledger/fabric-protos-go-apiv2/msp" 17 | "github.com/hyperledger/fabric-protos-go-apiv2/peer" 18 | "github.com/pkg/errors" 19 | yaml "gopkg.in/yaml.v2" 20 | ) 21 | 22 | type TracingProposal struct { 23 | *peer.Proposal 24 | TxId string 25 | Span opentracing.Span 26 | } 27 | 28 | type Elements struct { 29 | TxId string 30 | Span opentracing.Span 31 | EndorsementSpan opentracing.Span 32 | SignedProp *peer.SignedProposal 33 | Responses []*peer.ProposalResponse 34 | Orgs []string 35 | Processed bool 36 | Lock sync.Mutex 37 | } 38 | 39 | type TracingEnvelope struct { 40 | Env *common.Envelope 41 | TxId string 42 | Span opentracing.Span 43 | } 44 | 45 | type Config struct { 46 | Endorsers []Node `yaml:"endorsers"` 47 | Committers []Node `yaml:"committers"` 48 | CommitThreshold int `yaml:"commitThreshold"` 49 | Orderer Node `yaml:"orderer"` 50 | PolicyFile string `yaml:"policyFile"` 51 | Rule string 52 | Channel string `yaml:"channel"` 53 | Chaincode string `yaml:"chaincode"` 54 | Version string `yaml:"version"` 55 | Args []string `yaml:"args"` 56 | MSPID string `yaml:"mspid"` 57 | PrivateKey string `yaml:"private_key"` 58 | SignCert string `yaml:"sign_cert"` 59 | NumOfConn int `yaml:"num_of_conn"` 60 | ClientPerConn int `yaml:"client_per_conn"` 61 | } 62 | 63 | type Node struct { 64 | Addr string `yaml:"addr"` 65 | SslTargetNameOverride string `yaml:"ssl_target_name_override"` 66 | TLSCACert string `yaml:"tls_ca_cert"` 67 | Org string `yaml:"org"` 68 | TLSCAKey string `yaml:"tls_ca_key"` 69 | TLSCARoot string `yaml:"tls_ca_root"` 70 | TLSCACertByte []byte 71 | TLSCAKeyByte []byte 72 | TLSCARootByte []byte 73 | } 74 | 75 | func LoadConfig(f string) (Config, error) { 76 | config := Config{} 77 | raw, err := os.ReadFile(f) 78 | if err != nil { 79 | return config, errors.Wrapf(err, "error loading %s", f) 80 | } 81 | err = yaml.Unmarshal(raw, &config) 82 | if err != nil { 83 | return config, errors.Wrapf(err, "error unmarshal %s", f) 84 | } 85 | 86 | if len(config.PolicyFile) == 0 && config.PolicyFile == "" { 87 | return config, errors.New("empty endorsement policy") 88 | } 89 | 90 | // config.Rule read from PolicyFile 91 | in, err := os.ReadFile(config.PolicyFile) 92 | if err != nil { 93 | return config, err 94 | } 95 | config.Rule = string(in) 96 | 97 | for i := range config.Endorsers { 98 | err = config.Endorsers[i].LoadConfig() 99 | if err != nil { 100 | return config, err 101 | } 102 | } 103 | for i := range config.Committers { 104 | err = config.Committers[i].LoadConfig() 105 | if err != nil { 106 | return config, err 107 | } 108 | } 109 | err = config.Orderer.LoadConfig() 110 | if err != nil { 111 | return config, err 112 | } 113 | return config, nil 114 | } 115 | 116 | func (c Config) LoadCrypto() (*CryptoImpl, error) { 117 | conf := CryptoConfig{ 118 | MSPID: c.MSPID, 119 | PrivKey: c.PrivateKey, 120 | SignCert: c.SignCert, 121 | } 122 | 123 | priv, err := GetPrivateKey(conf.PrivKey) 124 | if err != nil { 125 | return nil, errors.Wrapf(err, "error loading priv key") 126 | } 127 | 128 | cert, certBytes, err := GetCertificate(conf.SignCert) 129 | if err != nil { 130 | return nil, errors.Wrapf(err, "error loading certificate") 131 | } 132 | 133 | id := &msp.SerializedIdentity{ 134 | Mspid: conf.MSPID, 135 | IdBytes: certBytes, 136 | } 137 | 138 | name, err := proto.Marshal(id) 139 | if err != nil { 140 | return nil, errors.Wrapf(err, "error get msp id") 141 | } 142 | 143 | return &CryptoImpl{ 144 | Creator: name, 145 | PrivKey: priv, 146 | SignCert: cert, 147 | }, nil 148 | } 149 | 150 | func GetTLSCACerts(file string) ([]byte, error) { 151 | if len(file) == 0 { 152 | return nil, nil 153 | } 154 | 155 | in, err := os.ReadFile(file) 156 | if err != nil { 157 | return nil, errors.Wrapf(err, "error loading %s", file) 158 | } 159 | return in, nil 160 | } 161 | 162 | func (n *Node) LoadConfig() error { 163 | TLSCACert, err := GetTLSCACerts(n.TLSCACert) 164 | if err != nil { 165 | return errors.Wrapf(err, "fail to load TLS CA Cert %s", n.TLSCACert) 166 | } 167 | certPEM, err := GetTLSCACerts(n.TLSCAKey) 168 | if err != nil { 169 | return errors.Wrapf(err, "fail to load TLS CA Key %s", n.TLSCAKey) 170 | } 171 | TLSCARoot, err := GetTLSCACerts(n.TLSCARoot) 172 | if err != nil { 173 | return errors.Wrapf(err, "fail to load TLS CA Root %s", n.TLSCARoot) 174 | } 175 | n.TLSCACertByte = TLSCACert 176 | n.TLSCAKeyByte = certPEM 177 | n.TLSCARootByte = TLSCARoot 178 | return nil 179 | } 180 | 181 | func GetPrivateKey(f string) (*ecdsa.PrivateKey, error) { 182 | in, err := os.ReadFile(f) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | k, err := utils.PEMtoPrivateKey(in, []byte{}) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | key, ok := k.(*ecdsa.PrivateKey) 193 | if !ok { 194 | return nil, errors.Errorf("expecting ecdsa key") 195 | } 196 | 197 | return key, nil 198 | } 199 | 200 | func GetCertificate(f string) (*x509.Certificate, []byte, error) { 201 | in, err := os.ReadFile(f) 202 | if err != nil { 203 | return nil, nil, err 204 | } 205 | 206 | block, _ := pem.Decode(in) 207 | 208 | c, err := x509.ParseCertificate(block.Bytes) 209 | return c, in, err 210 | } 211 | -------------------------------------------------------------------------------- /pkg/infra/observer/observer_test.go: -------------------------------------------------------------------------------- 1 | package observer_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "sync" 7 | "time" 8 | 9 | "github.com/hyperledger-twgc/tape/e2e" 10 | "github.com/hyperledger-twgc/tape/e2e/mock" 11 | "github.com/hyperledger-twgc/tape/pkg/infra/basic" 12 | "github.com/hyperledger-twgc/tape/pkg/infra/observer" 13 | 14 | . "github.com/onsi/ginkgo/v2" 15 | . "github.com/onsi/gomega" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | var _ = Describe("Observer", func() { 20 | var ( 21 | tmpDir string 22 | logger *log.Logger 23 | PolicyFile, mtlsCertFile, mtlsKeyFile *os.File 24 | ) 25 | type key string 26 | 27 | const start key = "start" 28 | 29 | BeforeEach(func() { 30 | logger = log.New() 31 | var err error 32 | tmpDir, err = os.MkdirTemp("", "tape-") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | mtlsCertFile, err = os.CreateTemp(tmpDir, "mtls-*.crt") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | mtlsKeyFile, err = os.CreateTemp(tmpDir, "mtls-*.key") 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | PolicyFile, err = os.CreateTemp(tmpDir, "policy") 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | err = e2e.GenerateCertAndKeys(mtlsKeyFile, mtlsCertFile) 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | err = e2e.GeneratePolicy(PolicyFile) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | PolicyFile.Close() 51 | mtlsCertFile.Close() 52 | mtlsKeyFile.Close() 53 | }) 54 | 55 | AfterEach(func() { 56 | os.RemoveAll(tmpDir) 57 | }) 58 | 59 | It("It should work with mock", func() { 60 | txC := make(chan struct{}, mock.MockTxSize) 61 | mpeer, err := mock.NewPeer(txC, nil) 62 | Expect(err).NotTo(HaveOccurred()) 63 | go mpeer.Start() 64 | defer mpeer.Stop() 65 | configFile, err := os.CreateTemp(tmpDir, "config*.yaml") 66 | Expect(err).NotTo(HaveOccurred()) 67 | paddrs := make([]string, 0) 68 | paddrs = append(paddrs, mpeer.Addrs()) 69 | 70 | configValue := e2e.Values{ 71 | PrivSk: mtlsKeyFile.Name(), 72 | SignCert: mtlsCertFile.Name(), 73 | Mtls: false, 74 | PeersAddrs: paddrs, 75 | OrdererAddr: "", 76 | CommitThreshold: 1, 77 | PolicyFile: PolicyFile.Name(), 78 | } 79 | e2e.GenerateConfigFile(configFile.Name(), configValue) 80 | config, err := basic.LoadConfig(configFile.Name()) 81 | Expect(err).NotTo(HaveOccurred()) 82 | crypto, err := config.LoadCrypto() 83 | Expect(err).NotTo(HaveOccurred()) 84 | 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | ctx = context.WithValue(ctx, start, time.Now()) 87 | defer cancel() 88 | errorCh := make(chan error, 10) 89 | blockCh := make(chan *observer.AddressedBlock) 90 | 91 | observers, err := observer.CreateObservers(ctx, crypto, errorCh, blockCh, config, logger) 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | finishCh := make(chan struct{}) 95 | var once sync.Once 96 | blockCollector, err := observer.NewBlockCollector(config.CommitThreshold, len(config.Committers), ctx, blockCh, finishCh, mock.MockTxSize, false, logger, &once, true) 97 | Expect(err).NotTo(HaveOccurred()) 98 | go blockCollector.Start() 99 | go observers.Start() 100 | go func() { 101 | for i := 0; i < mock.MockTxSize; i++ { 102 | txC <- struct{}{} 103 | } 104 | }() 105 | Eventually(finishCh).Should(BeClosed()) 106 | completed := time.Now() 107 | Expect(ctx.Value(start).(time.Time).Sub(completed)).Should(BeNumerically("<", 0.002), "observer with mock shouldn't take too long.") 108 | }) 109 | 110 | It("It should work as 2 committed of 3 peers", func() { 111 | 112 | TotalPeers := 3 113 | CommitThreshold := 2 114 | paddrs := make([]string, 0) 115 | txCs := make([]chan struct{}, 0) 116 | var mpeers []*mock.Peer 117 | 118 | for i := 0; i < TotalPeers; i++ { 119 | txC := make(chan struct{}, mock.MockTxSize) 120 | mpeer, err := mock.NewPeer(txC, nil) 121 | Expect(err).NotTo(HaveOccurred()) 122 | go mpeer.Start() 123 | defer mpeer.Stop() 124 | 125 | paddrs = append(paddrs, mpeer.Addrs()) 126 | mpeers = append(mpeers, mpeer) 127 | txCs = append(txCs, txC) 128 | } 129 | 130 | configFile, err := os.CreateTemp(tmpDir, "config*.yaml") 131 | Expect(err).NotTo(HaveOccurred()) 132 | configValue := e2e.Values{ 133 | PrivSk: mtlsKeyFile.Name(), 134 | SignCert: mtlsCertFile.Name(), 135 | Mtls: false, 136 | PeersAddrs: paddrs, 137 | OrdererAddr: "", 138 | CommitThreshold: CommitThreshold, 139 | PolicyFile: PolicyFile.Name(), 140 | } 141 | e2e.GenerateConfigFile(configFile.Name(), configValue) 142 | config, err := basic.LoadConfig(configFile.Name()) 143 | Expect(err).NotTo(HaveOccurred()) 144 | crypto, err := config.LoadCrypto() 145 | Expect(err).NotTo(HaveOccurred()) 146 | 147 | ctx, cancel := context.WithCancel(context.Background()) 148 | ctx = context.WithValue(ctx, start, time.Now()) 149 | defer cancel() 150 | 151 | blockCh := make(chan *observer.AddressedBlock) 152 | errorCh := make(chan error, 10) 153 | 154 | observers, err := observer.CreateObservers(ctx, crypto, errorCh, blockCh, config, logger) 155 | Expect(err).NotTo(HaveOccurred()) 156 | 157 | finishCh := make(chan struct{}) 158 | var once sync.Once 159 | blockCollector, err := observer.NewBlockCollector(config.CommitThreshold, len(config.Committers), ctx, blockCh, finishCh, mock.MockTxSize, true, logger, &once, true) 160 | Expect(err).NotTo(HaveOccurred()) 161 | go blockCollector.Start() 162 | go observers.Start() 163 | for i := 0; i < TotalPeers; i++ { 164 | go func(k int) { 165 | for j := 0; j < mock.MockTxSize; j++ { 166 | txCs[k] <- struct{}{} 167 | } 168 | }(i) 169 | } 170 | for i := 0; i < CommitThreshold; i++ { 171 | mpeers[i].Pause() 172 | } 173 | Consistently(finishCh).ShouldNot(Receive()) 174 | for i := 0; i < CommitThreshold; i++ { 175 | mpeers[i].Unpause() 176 | } 177 | Eventually(finishCh).Should(BeClosed()) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /cmd/tape/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hyperledger-twgc/tape/pkg/infra" 8 | "github.com/hyperledger-twgc/tape/pkg/infra/cmdImpl" 9 | 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | kingpin "gopkg.in/alecthomas/kingpin.v2" 13 | ) 14 | 15 | const ( 16 | loglevel = "TAPE_LOGLEVEL" 17 | logfilename = "Tape.log" 18 | DEFAULT_PROMETHEUS_ADDR = ":8080" 19 | ) 20 | 21 | var ( 22 | app = kingpin.New("tape", "A performance test tool for Hyperledger Fabric") 23 | 24 | con = app.Flag("config", "Path to config file").Short('c').String() 25 | num = app.Flag("number", "Number of tx for shot").Short('n').Int() 26 | rate = app.Flag("rate", "[Optional] Creates tx rate, default 0 as unlimited").Default("0").Float64() 27 | burst = app.Flag("burst", "[Optional] Burst size for Tape, should bigger than rate").Default("1000").Int() 28 | signerNumber = app.Flag("signers", "[Optional] signer parallel Number for Tape, default as 5").Default("5").Int() 29 | parallelNumber = app.Flag("parallel", "[Optional] parallel Number for Tape, default as 1").Default("1").Int() 30 | enablePrometheus = app.Flag("prometheus", "[Optional] prometheus enable or not").Default("false").Bool() 31 | prometheusAddr = app.Flag("prometheus-addr", "[Optional] prometheus address, default as :8080").String() 32 | 33 | run = app.Command("run", "Start the tape program").Default() 34 | 35 | version = app.Command("version", "Show version information") 36 | 37 | commitOnly = app.Command("commitOnly", "Start tape with commitOnly mode, starts dummy envelop for test orderer only") 38 | 39 | endorsementOnly = app.Command("endorsementOnly", "Start tape with endorsementOnly mode, starts endorsement and end") 40 | 41 | trafficOnly = app.Command("traffic", "Start tape with traffic mode") 42 | 43 | observerOnly = app.Command("observer", "Start tape with observer mode") 44 | ) 45 | 46 | func main() { 47 | var err error 48 | 49 | logger := log.New() 50 | logger.SetLevel(log.WarnLevel) 51 | file, err := os.OpenFile(logfilename, os.O_CREATE|os.O_WRONLY, 0755) 52 | if err != nil { 53 | panic(err) 54 | } 55 | defer file.Close() 56 | logger.SetOutput(file) 57 | if customerLevel, customerSet := os.LookupEnv(loglevel); customerSet { 58 | if lvl, err_lvl := log.ParseLevel(customerLevel); err_lvl == nil { 59 | logger.SetLevel(lvl) 60 | } 61 | } 62 | 63 | fullCmd := kingpin.MustParse(app.Parse(os.Args[1:])) 64 | switch fullCmd { 65 | case version.FullCommand(): 66 | fmt.Println(cmdImpl.GetVersionInfo()) 67 | case commitOnly.FullCommand(): 68 | checkArgs(rate, burst, signerNumber, parallelNumber, *con, *enablePrometheus, prometheusAddr, logger) 69 | err = cmdImpl.Process(*con, *num, *burst, *signerNumber, *parallelNumber, *rate, *enablePrometheus, *prometheusAddr, logger, infra.COMMIT) 70 | case endorsementOnly.FullCommand(): 71 | checkArgs(rate, burst, signerNumber, parallelNumber, *con, *enablePrometheus, prometheusAddr, logger) 72 | err = cmdImpl.Process(*con, *num, *burst, *signerNumber, *parallelNumber, *rate, *enablePrometheus, *prometheusAddr, logger, infra.ENDORSEMENT) 73 | case run.FullCommand(): 74 | checkArgs(rate, burst, signerNumber, parallelNumber, *con, *enablePrometheus, prometheusAddr, logger) 75 | err = cmdImpl.Process(*con, *num, *burst, *signerNumber, *parallelNumber, *rate, *enablePrometheus, *prometheusAddr, logger, infra.FULLPROCESS) 76 | case trafficOnly.FullCommand(): 77 | checkArgs(rate, burst, signerNumber, parallelNumber, *con, *enablePrometheus, prometheusAddr, logger) 78 | err = cmdImpl.Process(*con, *num, *burst, *signerNumber, *parallelNumber, *rate, *enablePrometheus, *prometheusAddr, logger, infra.TRAFFIC) 79 | case observerOnly.FullCommand(): 80 | checkArgs(rate, burst, signerNumber, parallelNumber, *con, *enablePrometheus, prometheusAddr, logger) 81 | err = cmdImpl.Process(*con, *num, *burst, *signerNumber, *parallelNumber, *rate, *enablePrometheus, *prometheusAddr, logger, infra.OBSERVER) 82 | default: 83 | err = errors.Errorf("invalid command: %s", fullCmd) 84 | } 85 | 86 | if err != nil { 87 | logger.Error(err) 88 | fmt.Fprint(os.Stderr, err) 89 | os.Exit(1) 90 | } 91 | os.Exit(0) 92 | } 93 | 94 | func checkArgs(rate *float64, burst, signerNumber, parallel *int, con string, enablePrometheus bool, prometheusAddr *string, logger *log.Logger) { 95 | if len(con) == 0 { 96 | os.Stderr.WriteString("tape: error: required flag --config not provided, try --help") 97 | os.Exit(1) 98 | } 99 | if *rate < 0 { 100 | os.Stderr.WriteString("tape: error: rate must be zero (unlimited) or positive number\n") 101 | os.Exit(1) 102 | } 103 | if *burst < 1 { 104 | os.Stderr.WriteString("tape: error: burst at least 1\n") 105 | os.Exit(1) 106 | } 107 | if *signerNumber < 1 { 108 | os.Stderr.WriteString("tape: error: signerNumber at least 1\n") 109 | os.Exit(1) 110 | } 111 | if *parallel < 1 { 112 | os.Stderr.WriteString("tape: error: parallel at least 1\n") 113 | os.Exit(1) 114 | } 115 | 116 | if int64(*rate) > int64(*burst) { 117 | fmt.Printf("As rate %d is bigger than burst %d, real rate is burst\n", int64(*rate), int64(*burst)) 118 | } 119 | 120 | // enable prometheus but not provide --prometheus-addr option, use default prometheus address ":8080" 121 | if enablePrometheus { 122 | if len(*prometheusAddr) == 0 { 123 | *prometheusAddr = DEFAULT_PROMETHEUS_ADDR 124 | } 125 | logger.Infof("prometheus running at %s\n", *prometheusAddr) 126 | } 127 | 128 | // not enable prometheus but provide --prometheus-addr option, show help message 129 | if !enablePrometheus && len(*prometheusAddr) != 0 { 130 | fmt.Printf("You've provided the --prometheus-addr option to specify a Prometheus address, but you haven't enabled Prometheus using --prometheus option\n") 131 | logger.Warnf("prometheus is not available at %s, because --prometheus option is not provided\n", *prometheusAddr) 132 | } 133 | 134 | logger.Infof("Will use rate %f as send rate\n", *rate) 135 | logger.Infof("Will use %d as burst\n", burst) 136 | } 137 | --------------------------------------------------------------------------------