mirror of
https://github.com/tendermint/tendermint.git
synced 2026-08-28 11:56:15 +00:00
Merge branch 'master' into wb/pbts-runbook
This commit is contained in:
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: |
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v1.13.0
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
working-directory: test/e2e
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'v0.34.x'
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'v0.35.x'
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
working-directory: test/e2e
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.17'
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install go-fuzz
|
||||
working-directory: test/fuzz
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the Jepsen repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'tendermint/jepsen'
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@1.0.13
|
||||
with:
|
||||
folder-path: "docs"
|
||||
|
||||
@@ -13,17 +13,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 8
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17'
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
**/**.go
|
||||
go.mod
|
||||
go.sum
|
||||
- uses: golangci/golangci-lint-action@v2.5.2
|
||||
- uses: golangci/golangci-lint-action@v3.1.0
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.42.1
|
||||
version: v1.44
|
||||
args: --timeout 10m
|
||||
github-token: ${{ secrets.github_token }}
|
||||
if: env.GIT_DIFF
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3
|
||||
- name: Lint Code Base
|
||||
uses: docker://github/super-linter:v4
|
||||
env:
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
name: Check Markdown links
|
||||
# TODO: Re-enable when https://github.com/gaurav-nelson/github-action-markdown-link-check/pull/126 lands.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@1.0.13
|
||||
with:
|
||||
check-modified-files-only: 'yes'
|
||||
#name: Check Markdown links
|
||||
#
|
||||
#on:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# pull_request:
|
||||
# branches: [master]
|
||||
#
|
||||
#jobs:
|
||||
# markdown-link-check:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - uses: gaurav-nelson/github-action-markdown-link-check@v1.0.13
|
||||
# with:
|
||||
# check-modified-files-only: 'yes'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Proto Check
|
||||
# Protobuf runs buf (https://buf.build/) lint and check-breakage
|
||||
# This workflow is only run when a file in the proto directory
|
||||
# has been modified.
|
||||
on:
|
||||
workflow_dispatch: # allow running workflow manually
|
||||
pull_request:
|
||||
paths:
|
||||
- "proto/*"
|
||||
jobs:
|
||||
proto-lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 4
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: lint
|
||||
run: make proto-lint
|
||||
proto-breakage:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 4
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: check-breakage
|
||||
run: make proto-check-breaking-ci
|
||||
@@ -1,64 +0,0 @@
|
||||
# This workflow (re)builds and pushes a Docker image containing the
|
||||
# protobuf build tools used by the other workflows.
|
||||
#
|
||||
# When making changes that require updates to the builder image, you
|
||||
# should merge the updates first and wait for this workflow to complete,
|
||||
# so that the changes will be available for the dependent workflows.
|
||||
#
|
||||
|
||||
name: Build & Push Proto Builder Image
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "proto/*"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "proto/*"
|
||||
schedule:
|
||||
# run this job once a month to recieve any go or buf updates
|
||||
- cron: "0 9 1 * *"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: tendermint/docker-build-proto
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: Check out and assign tags
|
||||
id: prep
|
||||
run: |
|
||||
DOCKER_IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
VERSION=noop
|
||||
if [[ "$GITHUB_REF" == "refs/tags/*" ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
elif [[ "$GITHUB_REF" == "refs/heads/*" ]]; then
|
||||
VERSION="$(echo "${GITHUB_REF#refs/heads/}" | sed -r 's#/+#-#g')"
|
||||
if [[ "${{ github.event.repository.default_branch }}" = "$VERSION" ]]; then
|
||||
VERSION=latest
|
||||
fi
|
||||
fi
|
||||
TAGS="${DOCKER_IMAGE}:${VERSION}"
|
||||
echo ::set-output name=tags::"${TAGS}"
|
||||
|
||||
- name: Set up docker buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v1.13.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and publish image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: ./proto
|
||||
file: ./proto/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.prep.outputs.tags }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Protobuf Lint
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'proto/**'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'proto/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: bufbuild/buf-setup-action@v1.1.0
|
||||
- uses: bufbuild/buf-lint-action@v1
|
||||
with:
|
||||
input: 'proto'
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: technote-space/get-diff-action@v6.0.1
|
||||
with:
|
||||
PATTERNS: |
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
Friendly reminder: We have a [bug bounty program](https://hackerone.com/cosmos).
|
||||
|
||||
## v0.35.2
|
||||
|
||||
February 28, 2022
|
||||
|
||||
Special thanks to external contributors on this release: @ashcherbakov, @yihuang, @waelsy123
|
||||
|
||||
### IMPROVEMENTS
|
||||
|
||||
- [consensus] [\#7875](https://github.com/tendermint/tendermint/pull/7875) additional timing metrics. (@williambanfield)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
- [abci] [\#7990](https://github.com/tendermint/tendermint/pull/7990) revert buffer limit change. (@williambanfield)
|
||||
- [cli] [#7837](https://github.com/tendermint/tendermint/pull/7837) fix app hash in state rollback. (@yihuang)
|
||||
- [cli] [\#7869](https://github.com/tendermint/tendermint/pull/7869) Update unsafe-reset-all command to match release v35. (waelsy123)
|
||||
- [light] [\#7640](https://github.com/tendermint/tendermint/pull/7640) Light Client: fix absence proof verification (@ashcherbakov)
|
||||
- [light] [\#7641](https://github.com/tendermint/tendermint/pull/7641) Light Client: fix querying against the latest height (@ashcherbakov)
|
||||
- [mempool] [\#7718](https://github.com/tendermint/tendermint/pull/7718) return duplicate tx errors more consistently. (@tychoish)
|
||||
- [rpc] [\#7744](https://github.com/tendermint/tendermint/pull/7744) fix layout of endpoint list. (@creachadair)
|
||||
- [statesync] [\#7886](https://github.com/tendermint/tendermint/pull/7886) assert app version matches. (@cmwaters)
|
||||
|
||||
## v0.35.1
|
||||
|
||||
January 26, 2022
|
||||
|
||||
@@ -19,6 +19,7 @@ Special thanks to external contributors on this release:
|
||||
- [rpc] \#7713 Remove unused options for websocket clients. (@creachadair)
|
||||
- [config] \#7930 Add new event subscription options and defaults. (@creachadair)
|
||||
- [rpc] \#7982 Add new Events interface and deprecate Subscribe. (@creachadair)
|
||||
- [cli] \#8081 make the reset command safe to use. (@marbar3778)
|
||||
|
||||
- Apps
|
||||
|
||||
|
||||
+25
-3
@@ -105,11 +105,33 @@ specify exactly the dependency you want to update, eg.
|
||||
|
||||
## Protobuf
|
||||
|
||||
We use [Protocol Buffers](https://developers.google.com/protocol-buffers) along with [gogoproto](https://github.com/gogo/protobuf) to generate code for use across Tendermint Core.
|
||||
We use [Protocol Buffers](https://developers.google.com/protocol-buffers) along
|
||||
with [`gogoproto`](https://github.com/gogo/protobuf) to generate code for use
|
||||
across Tendermint Core.
|
||||
|
||||
For linting, checking breaking changes and generating proto stubs, we use [buf](https://buf.build/). If you would like to run linting and check if the changes you have made are breaking then you will need to have docker running locally. Then the linting cmd will be `make proto-lint` and the breaking changes check will be `make proto-check-breaking`.
|
||||
To generate proto stubs, lint, and check protos for breaking changes, you will
|
||||
need to install [buf](https://buf.build/) and `gogoproto`. Then, from the root
|
||||
of the repository, run:
|
||||
|
||||
We use [Docker](https://www.docker.com/) to generate the protobuf stubs. To generate the stubs yourself, make sure docker is running then run `make proto-gen`. This command uses the spec repo to get the necessary protobuf files for generating the go code. If you are modifying the proto files manually for changes in the core data structures, you will need to clone them into the go repo and comment out lines 22-37 of the file `./scripts/protocgen.sh`.
|
||||
```bash
|
||||
# Lint all of the .proto files in proto/tendermint
|
||||
make proto-lint
|
||||
|
||||
# Check if any of your local changes (prior to committing to the Git repository)
|
||||
# are breaking
|
||||
make proto-check-breaking
|
||||
|
||||
# Generate Go code from the .proto files in proto/tendermint
|
||||
make proto-gen
|
||||
```
|
||||
|
||||
To automatically format `.proto` files, you will need
|
||||
[`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) installed. Once
|
||||
installed, you can run:
|
||||
|
||||
```bash
|
||||
make proto-format
|
||||
```
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ endif
|
||||
|
||||
LD_FLAGS = -X github.com/tendermint/tendermint/version.TMVersion=$(VERSION)
|
||||
BUILD_FLAGS = -mod=readonly -ldflags "$(LD_FLAGS)"
|
||||
BUILD_IMAGE := ghcr.io/tendermint/docker-build-proto
|
||||
DOCKER_PROTO_BUILDER := docker run -v $(shell pwd):/workspace --workdir /workspace $(BUILD_IMAGE)
|
||||
CGO_ENABLED ?= 0
|
||||
|
||||
# handle nostrip
|
||||
@@ -73,41 +71,57 @@ install:
|
||||
|
||||
$(BUILDDIR)/:
|
||||
mkdir -p $@
|
||||
# The Docker image containing the generator, formatter, and linter.
|
||||
# This is generated by proto/Dockerfile. To update tools, make changes
|
||||
# there and run the Build & Push Proto Builder Image workflow.
|
||||
IMAGE := ghcr.io/tendermint/docker-build-proto:latest
|
||||
DOCKER_PROTO_BUILDER := docker run -v $(shell pwd):/workspace --workdir /workspace $(IMAGE)
|
||||
HTTPS_GIT := https://github.com/tendermint/tendermint.git
|
||||
|
||||
###############################################################################
|
||||
### Protobuf ###
|
||||
###############################################################################
|
||||
|
||||
proto-all: proto-lint proto-check-breaking
|
||||
.PHONY: proto-all
|
||||
check-proto-deps:
|
||||
ifeq (,$(shell which buf))
|
||||
$(error "buf is required for Protobuf building, linting and breakage checking. See https://docs.buf.build/installation for installation instructions.")
|
||||
endif
|
||||
ifeq (,$(shell which protoc-gen-gogofaster))
|
||||
$(error "gogofaster plugin for protoc is required. Run 'go install github.com/gogo/protobuf/protoc-gen-gogofaster@latest' to install")
|
||||
endif
|
||||
.PHONY: check-proto-deps
|
||||
|
||||
proto-gen:
|
||||
check-proto-format-deps:
|
||||
ifeq (,$(shell which clang-format))
|
||||
$(error "clang-format is required for Protobuf formatting. See instructions for your platform on how to install it.")
|
||||
endif
|
||||
.PHONY: check-proto-format-deps
|
||||
|
||||
proto-gen: check-proto-deps
|
||||
@echo "Generating Protobuf files"
|
||||
@$(DOCKER_PROTO_BUILDER) buf generate --template=./buf.gen.yaml --config ./buf.yaml
|
||||
@buf generate
|
||||
@mv ./proto/tendermint/abci/types.pb.go ./abci/types/
|
||||
.PHONY: proto-gen
|
||||
|
||||
proto-lint:
|
||||
@$(DOCKER_PROTO_BUILDER) buf lint --error-format=json --config ./buf.yaml
|
||||
# These targets are provided for convenience and are intended for local
|
||||
# execution only.
|
||||
proto-lint: check-proto-deps
|
||||
@echo "Linting Protobuf files"
|
||||
@buf lint
|
||||
.PHONY: proto-lint
|
||||
|
||||
proto-format:
|
||||
proto-format: check-proto-format-deps
|
||||
@echo "Formatting Protobuf files"
|
||||
@$(DOCKER_PROTO_BUILDER) find . -name '*.proto' -path "./proto/*" -exec clang-format -i {} \;
|
||||
@find . -name '*.proto' -path "./proto/*" -exec clang-format -i {} \;
|
||||
.PHONY: proto-format
|
||||
|
||||
proto-check-breaking:
|
||||
@$(DOCKER_PROTO_BUILDER) buf breaking --against .git --config ./buf.yaml
|
||||
proto-check-breaking: check-proto-deps
|
||||
@echo "Checking for breaking changes in Protobuf files against local branch"
|
||||
@echo "Note: This is only useful if your changes have not yet been committed."
|
||||
@echo " Otherwise read up on buf's \"breaking\" command usage:"
|
||||
@echo " https://docs.buf.build/breaking/usage"
|
||||
@buf breaking --against ".git"
|
||||
.PHONY: proto-check-breaking
|
||||
|
||||
proto-check-breaking-ci:
|
||||
@$(DOCKER_PROTO_BUILDER) buf breaking --against $(HTTPS_GIT) --config ./buf.yaml
|
||||
.PHONY: proto-check-breaking-ci
|
||||
# TODO: Should be removed when work on ABCI++ is complete.
|
||||
# For more information, see https://github.com/tendermint/tendermint/issues/8066
|
||||
abci-proto-gen:
|
||||
./scripts/abci-gen.sh
|
||||
.PHONY: abci-proto-gen
|
||||
|
||||
###############################################################################
|
||||
### Build ABCI ###
|
||||
@@ -222,9 +236,7 @@ build-docs:
|
||||
mkdir -p ~/output/$${path_prefix} ; \
|
||||
cp -r .vuepress/dist/* ~/output/$${path_prefix}/ ; \
|
||||
cp ~/output/$${path_prefix}/index.html ~/output ; \
|
||||
done < versions ; \
|
||||
mkdir -p ~/output/master ; \
|
||||
cp -r .vuepress/dist/* ~/output/master/
|
||||
done < versions ;
|
||||
.PHONY: build-docs
|
||||
|
||||
###############################################################################
|
||||
@@ -331,3 +343,4 @@ split-test-packages:$(BUILDDIR)/packages.txt
|
||||
split -d -n l/$(NUM_SPLIT) $< $<.
|
||||
test-group-%:split-test-packages
|
||||
cat $(BUILDDIR)/packages.txt.$* | xargs go test -mod=readonly -timeout=5m -race -coverprofile=$(BUILDDIR)/$*.profile.out
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ We keep a public up-to-date version of our roadmap [here](./docs/roadmap/roadmap
|
||||
- [Terra](https://www.terra.money/)
|
||||
- [Celestia](https://celestia.org/)
|
||||
- [Anoma](https://anoma.network/)
|
||||
- [Vocdoni](https://docs.vocdoni.io/)
|
||||
|
||||
### Research
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package abciclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
// Creator creates new ABCI clients.
|
||||
type Creator func(log.Logger) (Client, error)
|
||||
|
||||
// NewLocalCreator returns a Creator for the given app,
|
||||
// which will be running locally.
|
||||
func NewLocalCreator(app types.Application) Creator {
|
||||
return func(logger log.Logger) (Client, error) {
|
||||
return NewLocalClient(logger, app), nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRemoteCreator returns a Creator for the given address (e.g.
|
||||
// "192.168.0.1") and transport (e.g. "tcp"). Set mustConnect to true if you
|
||||
// want the client to connect before reporting success.
|
||||
func NewRemoteCreator(logger log.Logger, addr, transport string, mustConnect bool) Creator {
|
||||
return func(log.Logger) (Client, error) {
|
||||
remoteApp, err := NewClient(logger, addr, transport, mustConnect)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to proxy: %w", err)
|
||||
}
|
||||
|
||||
return remoteApp, nil
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package abciclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
"github.com/tendermint/tendermint/abci/server"
|
||||
"github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
)
|
||||
|
||||
func TestProperSyncCalls(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
app := slowApp{}
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
_, c := setupClientServer(ctx, t, logger, app)
|
||||
|
||||
resp := make(chan error, 1)
|
||||
go func() {
|
||||
rsp, err := c.FinalizeBlock(ctx, types.RequestFinalizeBlock{})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, c.Flush(ctx))
|
||||
assert.NotNil(t, rsp)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resp <- c.Error():
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
require.Fail(t, "No response arrived")
|
||||
case err, ok := <-resp:
|
||||
require.True(t, ok, "Must not close channel")
|
||||
assert.NoError(t, err, "This should return success")
|
||||
}
|
||||
}
|
||||
|
||||
func setupClientServer(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
logger log.Logger,
|
||||
app types.Application,
|
||||
) (service.Service, abciclient.Client) {
|
||||
t.Helper()
|
||||
|
||||
// some port between 20k and 30k
|
||||
port := 20000 + rand.Int31()%10000
|
||||
addr := fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
s, err := server.NewServer(logger, addr, "socket", app)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.Start(ctx))
|
||||
t.Cleanup(s.Wait)
|
||||
|
||||
c := abciclient.NewSocketClient(logger, addr, true)
|
||||
require.NoError(t, c.Start(ctx))
|
||||
t.Cleanup(c.Wait)
|
||||
|
||||
require.True(t, s.IsRunning())
|
||||
require.True(t, c.IsRunning())
|
||||
|
||||
return s, c
|
||||
}
|
||||
|
||||
type slowApp struct {
|
||||
types.BaseApplication
|
||||
}
|
||||
|
||||
func (slowApp) FinalizeBlock(req types.RequestFinalizeBlock) types.ResponseFinalizeBlock {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return types.ResponseFinalizeBlock{}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func addCommands(cmd *cobra.Command, logger log.Logger) {
|
||||
cmd.AddCommand(consoleCmd)
|
||||
cmd.AddCommand(echoCmd)
|
||||
cmd.AddCommand(infoCmd)
|
||||
cmd.AddCommand(deliverTxCmd)
|
||||
cmd.AddCommand(finalizeBlockCmd)
|
||||
cmd.AddCommand(checkTxCmd)
|
||||
cmd.AddCommand(commitCmd)
|
||||
cmd.AddCommand(versionCmd)
|
||||
@@ -150,10 +150,9 @@ where example.file looks something like:
|
||||
|
||||
check_tx 0x00
|
||||
check_tx 0xff
|
||||
deliver_tx 0x00
|
||||
finalize_block 0x00
|
||||
check_tx 0x00
|
||||
deliver_tx 0x01
|
||||
deliver_tx 0x04
|
||||
finalize_block 0x01 0x04 0xff
|
||||
info
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
@@ -169,7 +168,7 @@ This command opens an interactive console for running any of the other commands
|
||||
without opening a new connection each time
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
ValidArgs: []string{"echo", "info", "deliver_tx", "check_tx", "commit", "query"},
|
||||
ValidArgs: []string{"echo", "info", "finalize_block", "check_tx", "commit", "query"},
|
||||
RunE: cmdConsole,
|
||||
}
|
||||
|
||||
@@ -188,11 +187,11 @@ var infoCmd = &cobra.Command{
|
||||
RunE: cmdInfo,
|
||||
}
|
||||
|
||||
var deliverTxCmd = &cobra.Command{
|
||||
Use: "deliver_tx",
|
||||
Short: "deliver a new transaction to the application",
|
||||
Long: "deliver a new transaction to the application",
|
||||
Args: cobra.ExactArgs(1),
|
||||
var finalizeBlockCmd = &cobra.Command{
|
||||
Use: "finalize_block",
|
||||
Short: "deliver a block of transactions to the application",
|
||||
Long: "deliver a block of transactions to the application",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: cmdFinalizeBlock,
|
||||
}
|
||||
|
||||
@@ -426,7 +425,7 @@ func muxOnCommands(cmd *cobra.Command, pArgs []string) error {
|
||||
return cmdCheckTx(cmd, actualArgs)
|
||||
case "commit":
|
||||
return cmdCommit(cmd, actualArgs)
|
||||
case "deliver_tx":
|
||||
case "finalize_block":
|
||||
return cmdFinalizeBlock(cmd, actualArgs)
|
||||
case "echo":
|
||||
return cmdEcho(cmd, actualArgs)
|
||||
@@ -500,19 +499,23 @@ func cmdFinalizeBlock(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
printResponse(cmd, args, response{
|
||||
Code: codeBad,
|
||||
Log: "want the tx",
|
||||
Log: "Must provide at least one transaction",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
txBytes, err := stringOrHexToBytes(args[0])
|
||||
txs := make([][]byte, len(args))
|
||||
for i, arg := range args {
|
||||
txBytes, err := stringOrHexToBytes(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txs[i] = txBytes
|
||||
}
|
||||
res, err := client.FinalizeBlock(cmd.Context(), types.RequestFinalizeBlock{Txs: txs})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := client.FinalizeBlock(cmd.Context(), types.RequestFinalizeBlock{Txs: [][]byte{txBytes}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tx := range res.Txs {
|
||||
for _, tx := range res.TxResults {
|
||||
printResponse(cmd, args, response{
|
||||
Code: tx.Code,
|
||||
Data: tx.Data,
|
||||
|
||||
@@ -84,8 +84,8 @@ func testBulk(ctx context.Context, t *testing.T, logger log.Logger, app types.Ap
|
||||
// Send bulk request
|
||||
res, err := client.FinalizeBlock(ctx, rfb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, numDeliverTxs, len(res.Txs), "Number of txs doesn't match")
|
||||
for _, tx := range res.Txs {
|
||||
require.Equal(t, numDeliverTxs, len(res.TxResults), "Number of txs doesn't match")
|
||||
for _, tx := range res.TxResults {
|
||||
require.Equal(t, tx.Code, code.CodeTypeOK, "Tx failed")
|
||||
}
|
||||
|
||||
@@ -138,8 +138,8 @@ func testGRPCSync(ctx context.Context, t *testing.T, logger log.Logger, app type
|
||||
// Send request
|
||||
response, err := client.FinalizeBlock(ctx, &rfb)
|
||||
require.NoError(t, err, "Error in GRPC FinalizeBlock")
|
||||
require.Equal(t, numDeliverTxs, len(response.Txs), "Number of txs returned via GRPC doesn't match")
|
||||
for _, tx := range response.Txs {
|
||||
require.Equal(t, numDeliverTxs, len(response.TxResults), "Number of txs returned via GRPC doesn't match")
|
||||
for _, tx := range response.TxResults {
|
||||
require.Equal(t, tx.Code, code.CodeTypeOK, "Tx failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (app *Application) Info(req types.RequestInfo) types.ResponseInfo {
|
||||
}
|
||||
|
||||
// tx is either "val:pubkey!power" or "key=value" or just arbitrary bytes
|
||||
func (app *Application) handleTx(tx []byte) *types.ResponseDeliverTx {
|
||||
func (app *Application) handleTx(tx []byte) *types.ExecTxResult {
|
||||
// if it starts with "val:", update the validator set
|
||||
// format is "val:pubkey!power"
|
||||
if isValidatorTx(tx) {
|
||||
@@ -156,7 +156,7 @@ func (app *Application) handleTx(tx []byte) *types.ResponseDeliverTx {
|
||||
},
|
||||
}
|
||||
|
||||
return &types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events}
|
||||
return &types.ExecTxResult{Code: code.CodeTypeOK, Events: events}
|
||||
}
|
||||
|
||||
func (app *Application) Close() error {
|
||||
@@ -190,12 +190,12 @@ func (app *Application) FinalizeBlock(req types.RequestFinalizeBlock) types.Resp
|
||||
}
|
||||
}
|
||||
|
||||
respTxs := make([]*types.ResponseDeliverTx, len(req.Txs))
|
||||
respTxs := make([]*types.ExecTxResult, len(req.Txs))
|
||||
for i, tx := range req.Txs {
|
||||
respTxs[i] = app.handleTx(tx)
|
||||
}
|
||||
|
||||
return types.ResponseFinalizeBlock{Txs: respTxs, ValidatorUpdates: app.ValUpdates}
|
||||
return types.ResponseFinalizeBlock{TxResults: respTxs, ValidatorUpdates: app.ValUpdates}
|
||||
}
|
||||
|
||||
func (*Application) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx {
|
||||
@@ -284,7 +284,7 @@ func (app *Application) PrepareProposal(req types.RequestPrepareProposal) types.
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
|
||||
return types.ResponsePrepareProposal{BlockData: app.substPrepareTx(req.BlockData)}
|
||||
return types.ResponsePrepareProposal{TxRecords: app.substPrepareTx(req.Txs)}
|
||||
}
|
||||
|
||||
func (*Application) ProcessProposal(req types.RequestProcessProposal) types.ResponseProcessProposal {
|
||||
@@ -338,13 +338,13 @@ func isValidatorTx(tx []byte) bool {
|
||||
|
||||
// format is "val:pubkey!power"
|
||||
// pubkey is a base64-encoded 32-byte ed25519 key
|
||||
func (app *Application) execValidatorTx(tx []byte) *types.ResponseDeliverTx {
|
||||
func (app *Application) execValidatorTx(tx []byte) *types.ExecTxResult {
|
||||
tx = tx[len(ValidatorSetChangePrefix):]
|
||||
|
||||
// get the pubkey and power
|
||||
pubKeyAndPower := strings.Split(string(tx), "!")
|
||||
if len(pubKeyAndPower) != 2 {
|
||||
return &types.ResponseDeliverTx{
|
||||
return &types.ExecTxResult{
|
||||
Code: code.CodeTypeEncodingError,
|
||||
Log: fmt.Sprintf("Expected 'pubkey!power'. Got %v", pubKeyAndPower)}
|
||||
}
|
||||
@@ -353,7 +353,7 @@ func (app *Application) execValidatorTx(tx []byte) *types.ResponseDeliverTx {
|
||||
// decode the pubkey
|
||||
pubkey, err := base64.StdEncoding.DecodeString(pubkeyS)
|
||||
if err != nil {
|
||||
return &types.ResponseDeliverTx{
|
||||
return &types.ExecTxResult{
|
||||
Code: code.CodeTypeEncodingError,
|
||||
Log: fmt.Sprintf("Pubkey (%s) is invalid base64", pubkeyS)}
|
||||
}
|
||||
@@ -361,7 +361,7 @@ func (app *Application) execValidatorTx(tx []byte) *types.ResponseDeliverTx {
|
||||
// decode the power
|
||||
power, err := strconv.ParseInt(powerS, 10, 64)
|
||||
if err != nil {
|
||||
return &types.ResponseDeliverTx{
|
||||
return &types.ExecTxResult{
|
||||
Code: code.CodeTypeEncodingError,
|
||||
Log: fmt.Sprintf("Power (%s) is not an int", powerS)}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ func (app *Application) execValidatorTx(tx []byte) *types.ResponseDeliverTx {
|
||||
}
|
||||
|
||||
// add, update, or remove a validator
|
||||
func (app *Application) updateValidator(v types.ValidatorUpdate) *types.ResponseDeliverTx {
|
||||
func (app *Application) updateValidator(v types.ValidatorUpdate) *types.ExecTxResult {
|
||||
pubkey, err := encoding.PubKeyFromProto(v.PubKey)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't decode public key: %w", err))
|
||||
@@ -386,7 +386,7 @@ func (app *Application) updateValidator(v types.ValidatorUpdate) *types.Response
|
||||
}
|
||||
if !hasKey {
|
||||
pubStr := base64.StdEncoding.EncodeToString(pubkey.Bytes())
|
||||
return &types.ResponseDeliverTx{
|
||||
return &types.ExecTxResult{
|
||||
Code: code.CodeTypeUnauthorized,
|
||||
Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func (app *Application) updateValidator(v types.ValidatorUpdate) *types.Response
|
||||
// add or update validator
|
||||
value := bytes.NewBuffer(make([]byte, 0))
|
||||
if err := types.WriteMessage(&v, value); err != nil {
|
||||
return &types.ResponseDeliverTx{
|
||||
return &types.ExecTxResult{
|
||||
Code: code.CodeTypeEncodingError,
|
||||
Log: fmt.Sprintf("error encoding validator: %v", err)}
|
||||
}
|
||||
@@ -411,7 +411,7 @@ func (app *Application) updateValidator(v types.ValidatorUpdate) *types.Response
|
||||
// we only update the changes array if we successfully updated the tree
|
||||
app.ValUpdates = append(app.ValUpdates, v)
|
||||
|
||||
return &types.ResponseDeliverTx{Code: code.CodeTypeOK}
|
||||
return &types.ExecTxResult{Code: code.CodeTypeOK}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
@@ -420,26 +420,40 @@ func (app *Application) updateValidator(v types.ValidatorUpdate) *types.Response
|
||||
const PreparePrefix = "prepare"
|
||||
|
||||
func isPrepareTx(tx []byte) bool {
|
||||
return strings.HasPrefix(string(tx), PreparePrefix)
|
||||
return bytes.HasPrefix(tx, []byte(PreparePrefix))
|
||||
}
|
||||
|
||||
// execPrepareTx is noop. tx data is considered as placeholder
|
||||
// and is substitute at the PrepareProposal.
|
||||
func (app *Application) execPrepareTx(tx []byte) *types.ResponseDeliverTx {
|
||||
func (app *Application) execPrepareTx(tx []byte) *types.ExecTxResult {
|
||||
// noop
|
||||
return &types.ResponseDeliverTx{}
|
||||
return &types.ExecTxResult{}
|
||||
}
|
||||
|
||||
// substPrepareTx subst all the preparetx in the blockdata
|
||||
// to null string(could be any arbitrary string).
|
||||
func (app *Application) substPrepareTx(blockData [][]byte) [][]byte {
|
||||
// TODO: this mechanism will change with the current spec of PrepareProposal
|
||||
// We now have a special type for marking a tx as changed
|
||||
// substPrepareTx substitutes all the transactions prefixed with 'prepare' in the
|
||||
// proposal for transactions with the prefix strips.
|
||||
// It marks all of the original transactions as 'REMOVED' so that
|
||||
// Tendermint will remove them from its mempool.
|
||||
func (app *Application) substPrepareTx(blockData [][]byte) []*types.TxRecord {
|
||||
trs := make([]*types.TxRecord, len(blockData))
|
||||
var removed []*types.TxRecord
|
||||
for i, tx := range blockData {
|
||||
if isPrepareTx(tx) {
|
||||
blockData[i] = make([]byte, len(tx))
|
||||
removed = append(removed, &types.TxRecord{
|
||||
Tx: tx,
|
||||
Action: types.TxRecord_REMOVED,
|
||||
})
|
||||
trs[i] = &types.TxRecord{
|
||||
Tx: bytes.TrimPrefix(tx, []byte(PreparePrefix)),
|
||||
Action: types.TxRecord_ADDED,
|
||||
}
|
||||
continue
|
||||
}
|
||||
trs[i] = &types.TxRecord{
|
||||
Tx: tx,
|
||||
Action: types.TxRecord_UNMODIFIED,
|
||||
}
|
||||
}
|
||||
|
||||
return blockData
|
||||
return append(trs, removed...)
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ const (
|
||||
func testKVStore(t *testing.T, app types.Application, tx []byte, key, value string) {
|
||||
req := types.RequestFinalizeBlock{Txs: [][]byte{tx}}
|
||||
ar := app.FinalizeBlock(req)
|
||||
require.Equal(t, 1, len(ar.Txs))
|
||||
require.False(t, ar.Txs[0].IsErr())
|
||||
require.Equal(t, 1, len(ar.TxResults))
|
||||
require.False(t, ar.TxResults[0].IsErr())
|
||||
// repeating tx doesn't raise error
|
||||
ar = app.FinalizeBlock(req)
|
||||
require.Equal(t, 1, len(ar.Txs))
|
||||
require.False(t, ar.Txs[0].IsErr())
|
||||
require.Equal(t, 1, len(ar.TxResults))
|
||||
require.False(t, ar.TxResults[0].IsErr())
|
||||
// commit
|
||||
app.Commit()
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestPersistentKVStoreInfo(t *testing.T) {
|
||||
header := tmproto.Header{
|
||||
Height: height,
|
||||
}
|
||||
kvstore.FinalizeBlock(types.RequestFinalizeBlock{Hash: hash, Header: header, Height: height})
|
||||
kvstore.FinalizeBlock(types.RequestFinalizeBlock{Hash: hash, Header: header})
|
||||
kvstore.Commit()
|
||||
|
||||
resInfo = kvstore.Info(types.RequestInfo{})
|
||||
@@ -196,7 +196,6 @@ func makeApplyBlock(
|
||||
resFinalizeBlock := kvstore.FinalizeBlock(types.RequestFinalizeBlock{
|
||||
Hash: hash,
|
||||
Header: header,
|
||||
Height: height,
|
||||
Txs: txs,
|
||||
})
|
||||
|
||||
@@ -326,13 +325,13 @@ func runClientTests(ctx context.Context, t *testing.T, client abciclient.Client)
|
||||
func testClient(ctx context.Context, t *testing.T, app abciclient.Client, tx []byte, key, value string) {
|
||||
ar, err := app.FinalizeBlock(ctx, types.RequestFinalizeBlock{Txs: [][]byte{tx}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(ar.Txs))
|
||||
require.False(t, ar.Txs[0].IsErr())
|
||||
require.Equal(t, 1, len(ar.TxResults))
|
||||
require.False(t, ar.TxResults[0].IsErr())
|
||||
// repeating FinalizeBlock doesn't raise error
|
||||
ar, err = app.FinalizeBlock(ctx, types.RequestFinalizeBlock{Txs: [][]byte{tx}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(ar.Txs))
|
||||
require.False(t, ar.Txs[0].IsErr())
|
||||
require.Equal(t, 1, len(ar.TxResults))
|
||||
require.False(t, ar.TxResults[0].IsErr())
|
||||
// commit
|
||||
_, err = app.Commit(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func Commit(ctx context.Context, client abciclient.Client, hashExp []byte) error
|
||||
|
||||
func FinalizeBlock(ctx context.Context, client abciclient.Client, txBytes [][]byte, codeExp []uint32, dataExp []byte) error {
|
||||
res, _ := client.FinalizeBlock(ctx, types.RequestFinalizeBlock{Txs: txBytes})
|
||||
for i, tx := range res.Txs {
|
||||
for i, tx := range res.TxResults {
|
||||
code, data, log := tx.Code, tx.Data, tx.Log
|
||||
if code != codeExp[i] {
|
||||
fmt.Println("Failed test: FinalizeBlock")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
echo hello
|
||||
info
|
||||
commit
|
||||
deliver_tx "abc"
|
||||
finalize_block "abc"
|
||||
info
|
||||
commit
|
||||
query "abc"
|
||||
deliver_tx "def=xyz"
|
||||
finalize_block "def=xyz" "ghi=123"
|
||||
commit
|
||||
query "def"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
-> code: OK
|
||||
-> data.hex: 0x0000000000000000
|
||||
|
||||
> deliver_tx "abc"
|
||||
> finalize_block "abc"
|
||||
-> code: OK
|
||||
|
||||
> info
|
||||
@@ -33,12 +33,14 @@
|
||||
-> value: abc
|
||||
-> value.hex: 616263
|
||||
|
||||
> deliver_tx "def=xyz"
|
||||
> finalize_block "def=xyz" "ghi=123"
|
||||
-> code: OK
|
||||
> finalize_block "def=xyz" "ghi=123"
|
||||
-> code: OK
|
||||
|
||||
> commit
|
||||
-> code: OK
|
||||
-> data.hex: 0x0400000000000000
|
||||
-> data.hex: 0x0600000000000000
|
||||
|
||||
> query "def"
|
||||
-> code: OK
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
check_tx 0x00
|
||||
check_tx 0xff
|
||||
deliver_tx 0x00
|
||||
finalize_block 0x00
|
||||
check_tx 0x00
|
||||
deliver_tx 0x01
|
||||
deliver_tx 0x04
|
||||
finalize_block 0x01
|
||||
finalize_block 0x04
|
||||
info
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
> check_tx 0xff
|
||||
-> code: OK
|
||||
|
||||
> deliver_tx 0x00
|
||||
> finalize_block 0x00
|
||||
-> code: OK
|
||||
|
||||
> check_tx 0x00
|
||||
-> code: OK
|
||||
|
||||
> deliver_tx 0x01
|
||||
> finalize_block 0x01
|
||||
-> code: OK
|
||||
|
||||
> deliver_tx 0x04
|
||||
> finalize_block 0x04
|
||||
-> code: OK
|
||||
|
||||
> info
|
||||
-> code: OK
|
||||
-> data: {"hashes":0,"txs":3}
|
||||
-> data.hex: 0x7B22686173686573223A302C22747873223A337D
|
||||
-> data: {"size":3}
|
||||
-> data.hex: 0x7B2273697A65223A337D
|
||||
|
||||
|
||||
@@ -103,12 +103,12 @@ func (BaseApplication) ProcessProposal(req RequestProcessProposal) ResponseProce
|
||||
}
|
||||
|
||||
func (BaseApplication) FinalizeBlock(req RequestFinalizeBlock) ResponseFinalizeBlock {
|
||||
txs := make([]*ResponseDeliverTx, len(req.Txs))
|
||||
txs := make([]*ExecTxResult, len(req.Txs))
|
||||
for i := range req.Txs {
|
||||
txs[i] = &ResponseDeliverTx{Code: CodeTypeOK}
|
||||
txs[i] = &ExecTxResult{Code: CodeTypeOK}
|
||||
}
|
||||
return ResponseFinalizeBlock{
|
||||
Txs: txs,
|
||||
TxResults: txs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
b, err := json.Marshal(&ResponseDeliverTx{})
|
||||
b, err := json.Marshal(&ExecTxResult{Code: 1})
|
||||
assert.NoError(t, err)
|
||||
// include empty fields.
|
||||
assert.True(t, strings.Contains(string(b), "code"))
|
||||
|
||||
@@ -33,6 +33,16 @@ func (r ResponseDeliverTx) IsErr() bool {
|
||||
return r.Code != CodeTypeOK
|
||||
}
|
||||
|
||||
// IsOK returns true if Code is OK.
|
||||
func (r ExecTxResult) IsOK() bool {
|
||||
return r.Code == CodeTypeOK
|
||||
}
|
||||
|
||||
// IsErr returns true if Code is something other than OK.
|
||||
func (r ExecTxResult) IsErr() bool {
|
||||
return r.Code != CodeTypeOK
|
||||
}
|
||||
|
||||
// IsOK returns true if Code is OK.
|
||||
func (r ResponseQuery) IsOK() bool {
|
||||
return r.Code == CodeTypeOK
|
||||
@@ -157,3 +167,31 @@ func RespondVerifyVoteExtension(ok bool) ResponseVerifyVoteExtension {
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
// deterministicExecTxResult constructs a copy of response that omits
|
||||
// non-deterministic fields. The input response is not modified.
|
||||
func deterministicExecTxResult(response *ExecTxResult) *ExecTxResult {
|
||||
return &ExecTxResult{
|
||||
Code: response.Code,
|
||||
Data: response.Data,
|
||||
GasWanted: response.GasWanted,
|
||||
GasUsed: response.GasUsed,
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalTxResults encodes the the TxResults as a list of byte
|
||||
// slices. It strips off the non-deterministic pieces of the TxResults
|
||||
// so that the resulting data can be used for hash comparisons and used
|
||||
// in Merkle proofs.
|
||||
func MarshalTxResults(r []*ExecTxResult) ([][]byte, error) {
|
||||
s := make([][]byte, len(r))
|
||||
for i, e := range r {
|
||||
d := deterministicExecTxResult(e)
|
||||
b, err := d.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s[i] = b
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
+2755
-1522
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
)
|
||||
|
||||
func TestHashAndProveResults(t *testing.T) {
|
||||
trs := []*abci.ExecTxResult{
|
||||
// Note, these tests rely on the first two entries being in this order.
|
||||
{Code: 0, Data: nil},
|
||||
{Code: 0, Data: []byte{}},
|
||||
|
||||
{Code: 0, Data: []byte("one")},
|
||||
{Code: 14, Data: nil},
|
||||
{Code: 14, Data: []byte("foo")},
|
||||
{Code: 14, Data: []byte("bar")},
|
||||
}
|
||||
|
||||
// Nil and []byte{} should produce the same bytes
|
||||
bz0, err := trs[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
bz1, err := trs[1].Marshal()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bz0, bz1)
|
||||
|
||||
// Make sure that we can get a root hash from results and verify proofs.
|
||||
rs, err := abci.MarshalTxResults(trs)
|
||||
require.NoError(t, err)
|
||||
root := merkle.HashFromByteSlices(rs)
|
||||
assert.NotEmpty(t, root)
|
||||
|
||||
_, proofs := merkle.ProofsFromByteSlices(rs)
|
||||
for i, tr := range trs {
|
||||
bz, err := tr.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
valid := proofs[i].Verify(root, bz)
|
||||
assert.NoError(t, valid, "%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashDeterministicFieldsOnly(t *testing.T) {
|
||||
tr1 := abci.ExecTxResult{
|
||||
Code: 1,
|
||||
Data: []byte("transaction"),
|
||||
Log: "nondeterministic data: abc",
|
||||
Info: "nondeterministic data: abc",
|
||||
GasWanted: 1000,
|
||||
GasUsed: 1000,
|
||||
Events: []abci.Event{},
|
||||
Codespace: "nondeterministic.data.abc",
|
||||
}
|
||||
tr2 := abci.ExecTxResult{
|
||||
Code: 1,
|
||||
Data: []byte("transaction"),
|
||||
Log: "nondeterministic data: def",
|
||||
Info: "nondeterministic data: def",
|
||||
GasWanted: 1000,
|
||||
GasUsed: 1000,
|
||||
Events: []abci.Event{},
|
||||
Codespace: "nondeterministic.data.def",
|
||||
}
|
||||
r1, err := abci.MarshalTxResults([]*abci.ExecTxResult{&tr1})
|
||||
require.NoError(t, err)
|
||||
r2, err := abci.MarshalTxResults([]*abci.ExecTxResult{&tr2})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, merkle.HashFromByteSlices(r1), merkle.HashFromByteSlices(r2))
|
||||
}
|
||||
+7
-12
@@ -1,14 +1,9 @@
|
||||
# The version of the generation template (required).
|
||||
# The only currently-valid value is v1beta1.
|
||||
version: v1beta1
|
||||
|
||||
# The plugins to run.
|
||||
version: v1
|
||||
plugins:
|
||||
# The name of the plugin.
|
||||
- name: gogofaster
|
||||
# The directory where the generated proto output will be written.
|
||||
# The directory is relative to where the generation tool was run.
|
||||
out: proto
|
||||
# Set options to assign import paths to the well-known types
|
||||
# and to enable service generation.
|
||||
opt: Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration,plugins=grpc,paths=source_relative
|
||||
out: ./proto/
|
||||
opt:
|
||||
- Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types
|
||||
- Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration
|
||||
- plugins=grpc
|
||||
- paths=source_relative
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version: v1
|
||||
directories:
|
||||
- proto
|
||||
@@ -213,7 +213,7 @@ func eventReIndex(cmd *cobra.Command, args eventReIndexArgs) error {
|
||||
Height: b.Height,
|
||||
Index: uint32(i),
|
||||
Tx: b.Data.Txs[i],
|
||||
Result: *(r.FinalizeBlock.Txs[i]),
|
||||
Result: *(r.FinalizeBlock.TxResults[i]),
|
||||
}
|
||||
|
||||
_ = batch.Add(&tr)
|
||||
|
||||
@@ -153,10 +153,10 @@ func TestReIndexEvent(t *testing.T) {
|
||||
On("IndexTxEvents", mock.AnythingOfType("[]*types.TxResult")).Return(errors.New("")).Once().
|
||||
On("IndexTxEvents", mock.AnythingOfType("[]*types.TxResult")).Return(nil)
|
||||
|
||||
dtx := abcitypes.ResponseDeliverTx{}
|
||||
dtx := abcitypes.ExecTxResult{}
|
||||
abciResp := &prototmstate.ABCIResponses{
|
||||
FinalizeBlock: &abcitypes.ResponseFinalizeBlock{
|
||||
Txs: []*abcitypes.ResponseDeliverTx{&dtx},
|
||||
TxResults: []*abcitypes.ExecTxResult{&dtx},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -31,6 +32,20 @@ func MakeResetAllCommand(conf *config.Config, logger log.Logger) *cobra.Command
|
||||
return cmd
|
||||
}
|
||||
|
||||
// MakeResetStateCommand constructs a command that removes the database of
|
||||
// the specified Tendermint core instance.
|
||||
func MakeResetStateCommand(conf *config.Config, logger log.Logger) *cobra.Command {
|
||||
var keyType string
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "reset-state",
|
||||
Short: "Remove all the data and WAL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return resetState(conf.DBDir(), logger, keyType)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func MakeResetPrivateValidatorCommand(conf *config.Config, logger log.Logger) *cobra.Command {
|
||||
var keyType string
|
||||
|
||||
@@ -55,18 +70,76 @@ func MakeResetPrivateValidatorCommand(conf *config.Config, logger log.Logger) *c
|
||||
// it's only suitable for testnets.
|
||||
|
||||
// resetAll removes address book files plus all data, and resets the privValdiator data.
|
||||
// Exported so other CLI tools can use it.
|
||||
func resetAll(dbDir, privValKeyFile, privValStateFile string, logger log.Logger, keyType string) error {
|
||||
if err := os.RemoveAll(dbDir); err == nil {
|
||||
logger.Info("Removed all blockchain history", "dir", dbDir)
|
||||
} else {
|
||||
logger.Error("error removing all blockchain history", "dir", dbDir, "err", err)
|
||||
}
|
||||
// recreate the dbDir since the privVal state needs to live there
|
||||
|
||||
return resetFilePV(privValKeyFile, privValStateFile, logger, keyType)
|
||||
}
|
||||
|
||||
// resetState removes address book files plus all databases.
|
||||
func resetState(dbDir string, logger log.Logger, keyType string) error {
|
||||
blockdb := filepath.Join(dbDir, "blockstore.db")
|
||||
state := filepath.Join(dbDir, "state.db")
|
||||
wal := filepath.Join(dbDir, "cs.wal")
|
||||
evidence := filepath.Join(dbDir, "evidence.db")
|
||||
txIndex := filepath.Join(dbDir, "tx_index.db")
|
||||
peerstore := filepath.Join(dbDir, "peerstore.db")
|
||||
|
||||
if tmos.FileExists(blockdb) {
|
||||
if err := os.RemoveAll(blockdb); err == nil {
|
||||
logger.Info("Removed all blockstore.db", "dir", blockdb)
|
||||
} else {
|
||||
logger.Error("error removing all blockstore.db", "dir", blockdb, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tmos.FileExists(state) {
|
||||
if err := os.RemoveAll(state); err == nil {
|
||||
logger.Info("Removed all state.db", "dir", state)
|
||||
} else {
|
||||
logger.Error("error removing all state.db", "dir", state, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tmos.FileExists(wal) {
|
||||
if err := os.RemoveAll(wal); err == nil {
|
||||
logger.Info("Removed all cs.wal", "dir", wal)
|
||||
} else {
|
||||
logger.Error("error removing all cs.wal", "dir", wal, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tmos.FileExists(evidence) {
|
||||
if err := os.RemoveAll(evidence); err == nil {
|
||||
logger.Info("Removed all evidence.db", "dir", evidence)
|
||||
} else {
|
||||
logger.Error("error removing all evidence.db", "dir", evidence, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tmos.FileExists(txIndex) {
|
||||
if err := os.RemoveAll(txIndex); err == nil {
|
||||
logger.Info("Removed tx_index.db", "dir", txIndex)
|
||||
} else {
|
||||
logger.Error("error removing tx_index.db", "dir", txIndex, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tmos.FileExists(peerstore) {
|
||||
if err := os.RemoveAll(peerstore); err == nil {
|
||||
logger.Info("Removed peerstore.db", "dir", peerstore)
|
||||
} else {
|
||||
logger.Error("error removing peerstore.db", "dir", peerstore, "err", err)
|
||||
}
|
||||
}
|
||||
if err := tmos.EnsureDir(dbDir, 0700); err != nil {
|
||||
logger.Error("unable to recreate dbDir", "err", err)
|
||||
}
|
||||
return resetFilePV(privValKeyFile, privValStateFile, logger, keyType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetFilePV(privValKeyFile, privValStateFile string, logger log.Logger, keyType string) error {
|
||||
|
||||
@@ -34,6 +34,7 @@ func main() {
|
||||
commands.MakeReplayCommand(conf, logger),
|
||||
commands.MakeReplayConsoleCommand(conf, logger),
|
||||
commands.MakeResetAllCommand(conf, logger),
|
||||
commands.MakeResetStateCommand(conf, logger),
|
||||
commands.MakeResetPrivateValidatorCommand(conf, logger),
|
||||
commands.MakeShowValidatorCommand(conf, logger),
|
||||
commands.MakeTestnetFilesCommand(conf, logger),
|
||||
|
||||
+16
-16
@@ -27,17 +27,17 @@ Usage:
|
||||
abci-cli [command]
|
||||
|
||||
Available Commands:
|
||||
batch Run a batch of abci commands against an application
|
||||
check_tx Validate a tx
|
||||
commit Commit the application state and return the Merkle root hash
|
||||
console Start an interactive abci console for multiple commands
|
||||
deliver_tx Deliver a new tx to the application
|
||||
kvstore ABCI demo example
|
||||
echo Have the application echo a message
|
||||
help Help about any command
|
||||
info Get some info about the application
|
||||
query Query the application state
|
||||
set_option Set an options on the application
|
||||
batch Run a batch of abci commands against an application
|
||||
check_tx Validate a tx
|
||||
commit Commit the application state and return the Merkle root hash
|
||||
console Start an interactive abci console for multiple commands
|
||||
finalize_block Send a set of transactions to the application
|
||||
kvstore ABCI demo example
|
||||
echo Have the application echo a message
|
||||
help Help about any command
|
||||
info Get some info about the application
|
||||
query Query the application state
|
||||
set_option Set an options on the application
|
||||
|
||||
Flags:
|
||||
--abci string socket or grpc (default "socket")
|
||||
@@ -53,7 +53,7 @@ Use "abci-cli [command] --help" for more information about a command.
|
||||
The `abci-cli` tool lets us send ABCI messages to our application, to
|
||||
help build and debug them.
|
||||
|
||||
The most important messages are `deliver_tx`, `check_tx`, and `commit`,
|
||||
The most important messages are `finalize_block`, `check_tx`, and `commit`,
|
||||
but there are others for convenience, configuration, and information
|
||||
purposes.
|
||||
|
||||
@@ -173,7 +173,7 @@ Try running these commands:
|
||||
-> code: OK
|
||||
-> data.hex: 0x0000000000000000
|
||||
|
||||
> deliver_tx "abc"
|
||||
> finalize_block "abc"
|
||||
-> code: OK
|
||||
|
||||
> info
|
||||
@@ -192,7 +192,7 @@ Try running these commands:
|
||||
-> value: abc
|
||||
-> value.hex: 616263
|
||||
|
||||
> deliver_tx "def=xyz"
|
||||
> finalize_block "def=xyz"
|
||||
-> code: OK
|
||||
|
||||
> commit
|
||||
@@ -207,8 +207,8 @@ Try running these commands:
|
||||
-> value.hex: 78797A
|
||||
```
|
||||
|
||||
Note that if we do `deliver_tx "abc"` it will store `(abc, abc)`, but if
|
||||
we do `deliver_tx "abc=efg"` it will store `(abc, efg)`.
|
||||
Note that if we do `finalize_block "abc"` it will store `(abc, abc)`, but if
|
||||
we do `finalize_block "abc=efg"` it will store `(abc, efg)`.
|
||||
|
||||
Similarly, you could put the commands in a file and run
|
||||
`abci-cli --verbose batch < myfile`.
|
||||
|
||||
@@ -85,6 +85,11 @@ Note the context/background should be written in the present tense.
|
||||
- [ADR-067: Mempool Refactor](./adr-067-mempool-refactor.md)
|
||||
- [ADR-075: RPC Event Subscription Interface](./adr-075-rpc-subscription.md)
|
||||
- [ADR-076: Combine Spec and Tendermint Repositories](./adr-076-combine-spec-repo.md)
|
||||
- [ADR-081: Protocol Buffers Management](./adr-081-protobuf-mgmt.md)
|
||||
|
||||
### Deprecated
|
||||
|
||||
None
|
||||
|
||||
### Rejected
|
||||
|
||||
@@ -92,7 +97,6 @@ Note the context/background should be written in the present tense.
|
||||
- [ADR-029: Check-Tx-Consensus](./adr-029-check-tx-consensus.md)
|
||||
- [ADR-058: Event-Hashing](./adr-058-event-hashing.md)
|
||||
|
||||
|
||||
### Proposed
|
||||
|
||||
- [ADR-007: Trust-Metric-Usage](./adr-007-trust-metric-usage.md)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Changelog
|
||||
|
||||
- 01-Mar-2022: Update long-polling interface (@creachadair).
|
||||
- 10-Feb-2022: Updates to reflect implementation.
|
||||
- 26-Jan-2022: Marked accepted.
|
||||
- 22-Jan-2022: Updated and expanded (@creachadair).
|
||||
@@ -347,8 +348,8 @@ limit.
|
||||
|
||||
The `wait_time` parameter is used to effect polling. If `before` is empty and
|
||||
no items are available, the server will wait for up to `wait_time` for matching
|
||||
items to arrive at the head of the log. If `wait_time` is zero, the server will
|
||||
return whatever eligible items are available immediately.
|
||||
items to arrive at the head of the log. If `wait_time` is zero or negative, the
|
||||
server will wait for a default (positive) interval.
|
||||
|
||||
If `before` non-empty, `wait_time` is ignored: new results are only added to
|
||||
the head of the log, so there is no need to wait. This allows the client to
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# ADR 081: Protocol Buffers Management
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2022-02-28: First draft
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
[Tracking issue](https://github.com/tendermint/tendermint/issues/8121)
|
||||
|
||||
## Context
|
||||
|
||||
At present, we manage the [Protocol Buffers] schema files ("protos") that define
|
||||
our wire-level data formats within the Tendermint repository itself (see the
|
||||
[`proto`](../../proto/) directory). Recently, we have been making use of [Buf],
|
||||
both locally and in CI, in order to generate Go stubs, and lint and check
|
||||
`.proto` files for breaking changes.
|
||||
|
||||
The version of Buf used at the time of this decision was `v1beta1`, and it was
|
||||
discussed in [\#7975] and in weekly calls as to whether we should upgrade to
|
||||
`v1` and harmonize our approach with that used by the Cosmos SDK. The team
|
||||
managing the Cosmos SDK was primarily interested in having our protos versioned
|
||||
and easily accessible from the [Buf] registry.
|
||||
|
||||
The three main sets of stakeholders for the `.proto` files and their needs, as
|
||||
currently understood, are as follows.
|
||||
|
||||
1. Tendermint needs Go code generated from `.proto` files.
|
||||
2. Consumers of Tendermint's `.proto` files, specifically projects that want to
|
||||
interoperate with Tendermint and need to generate code for their own
|
||||
programming language, want to be able to access these files in a reliable and
|
||||
efficient way.
|
||||
3. The Tendermint Core team wants to provide stable interfaces that are as easy
|
||||
as possible to maintain, on which consumers can depend, and to be able to
|
||||
notify those consumers promptly when those interfaces change. To this end, we
|
||||
want to:
|
||||
1. Prevent any breaking changes from being introduced in minor/patch releases
|
||||
of Tendermint. Only major version updates should be able to contain
|
||||
breaking interface changes.
|
||||
2. Prevent generated code from diverging from the Protobuf schema files.
|
||||
|
||||
There was also discussion surrounding the notion of automated documentation
|
||||
generation and hosting, but it is not clear at this time whether this would be
|
||||
that valuable to any of our stakeholders. What will, of course, be valuable at
|
||||
minimum would be better documentation (in comments) of the `.proto` files
|
||||
themselves.
|
||||
|
||||
## Alternative Approaches
|
||||
|
||||
### Meeting stakeholders' needs
|
||||
|
||||
1. Go stub generation from protos. We could use:
|
||||
1. [Buf]. This approach has been rather cumbersome up to this point, and it
|
||||
is not clear what Buf really provides beyond that which `protoc` provides
|
||||
to justify the additional complexity in configuring Buf for stub
|
||||
generation.
|
||||
2. [protoc] - the Protocol Buffers compiler.
|
||||
2. Notification of breaking changes:
|
||||
1. Buf in CI for all pull requests to *release* branches only (and not on
|
||||
`master`).
|
||||
2. Buf in CI on every pull request to every branch (this was the case at the
|
||||
time of this decision, and the team decided that the signal-to-noise ratio
|
||||
for this approach was too low to be of value).
|
||||
3. `.proto` linting:
|
||||
1. Buf in CI on every pull request
|
||||
4. `.proto` formatting:
|
||||
1. [clang-format] locally and a [clang-format GitHub Action] in CI to check
|
||||
that files are formatted properly on every pull request.
|
||||
5. Sharing of `.proto` files in a versioned, reliable manner:
|
||||
1. Consumers could simply clone the Tendermint repository, check out a
|
||||
specific commit, tag or branch and manually copy out all of the `.proto`
|
||||
files they need. This requires no effort from the Tendermint Core team and
|
||||
will continue to be an option for consumers. The drawback of this approach
|
||||
is that it requires manual coding/scripting to implement and is brittle in
|
||||
the face of bigger changes.
|
||||
2. Uploading our `.proto` files to Buf's registry on every release. This is
|
||||
by far the most seamless for consumers of our `.proto` files, but requires
|
||||
the dependency on Buf. This has the additional benefit that the Buf
|
||||
registry will automatically [generate and host
|
||||
documentation][buf-docs-gen] for these protos.
|
||||
3. We could create a process that, upon release, creates a `.zip` file
|
||||
containing our `.proto` files.
|
||||
|
||||
### Popular alternatives to Buf
|
||||
|
||||
[Prototool] was not considered as it appears deprecated, and the ecosystem seems
|
||||
to be converging on Buf at this time.
|
||||
|
||||
### Tooling complexity
|
||||
|
||||
The more tools we have in our build/CI processes, the more complex and fragile
|
||||
repository/CI management becomes, and the longer it takes to onboard new team
|
||||
members. Maintainability is a core concern here.
|
||||
|
||||
### Buf sustainability and costs
|
||||
|
||||
One of the primary considerations regarding the usage of Buf is whether, for
|
||||
example, access to its registry will eventually become a
|
||||
paid-for/subscription-based service and whether this is valuable enough for us
|
||||
and the ecosystem to pay for such a service. At this time, it appears as though
|
||||
Buf will never charge for hosting open source projects' protos.
|
||||
|
||||
Another consideration was Buf's sustainability as a project - what happens when
|
||||
their resources run out? Will there be a strong and broad enough open source
|
||||
community to continue maintaining it?
|
||||
|
||||
### Local Buf usage options
|
||||
|
||||
Local usage of Buf (i.e. not in CI) can be accomplished in two ways:
|
||||
|
||||
1. Installing the relevant tools individually.
|
||||
2. By way of its [Docker image][buf-docker].
|
||||
|
||||
Local installation of Buf requires developers to manually keep their toolchains
|
||||
up-to-date. The Docker option comes with a number of complexities, including
|
||||
how the file system permissions of code generated by a Docker container differ
|
||||
between platforms (e.g. on Linux, Buf-generated code ends up being owned by
|
||||
`root`).
|
||||
|
||||
The trouble with the Docker-based approach is that we make use of the
|
||||
[gogoprotobuf] plugin for `protoc`. Continuing to use the Docker-based approach
|
||||
to using Buf will mean that we will have to continue building our own custom
|
||||
Docker image with embedded gogoprotobuf.
|
||||
|
||||
Along these lines, we could eventually consider coming up with a [Nix]- or
|
||||
[redo]-based approach to developer tooling to ensure tooling consistency across
|
||||
the team and for anyone who wants to be able to contribute to Tendermint.
|
||||
|
||||
## Decision
|
||||
|
||||
1. We will adopt Buf for now for proto generation, linting, breakage checking
|
||||
and its registry (mainly in CI, with optional usage locally).
|
||||
2. Failing CI when checking for breaking changes in `.proto` files will only
|
||||
happen when performing minor/patch releases.
|
||||
3. Local tooling will be favored over Docker-based tooling.
|
||||
|
||||
## Detailed Design
|
||||
|
||||
We currently aim to:
|
||||
|
||||
1. Update to Buf `v1` to facilitate linting, breakage checking and uploading to
|
||||
the Buf registry.
|
||||
2. Configure CI appropriately for proto management:
|
||||
1. Uploading protos to the Buf registry on every release (e.g. the
|
||||
[approach][cosmos-sdk-buf-registry-ci] used by the Cosmos SDK).
|
||||
2. Linting on every pull request (e.g. the
|
||||
[approach][cosmos-sdk-buf-linting-ci] used by the Cosmos SDK). The linter
|
||||
passing should be considered a requirement for accepting PRs.
|
||||
3. Checking for breaking changes in minor/patch version releases and failing
|
||||
CI accordingly - see [\#8003].
|
||||
4. Add [clang-format GitHub Action] to check `.proto` file formatting. Format
|
||||
checking should be considered a requirement for accepting PRs.
|
||||
3. Update the Tendermint [`Makefile`](../../Makefile) to primarily facilitate
|
||||
local Protobuf stub generation, linting, formatting and breaking change
|
||||
checking. More specifically:
|
||||
1. This includes removing the dependency on Docker and introducing the
|
||||
dependency on local toolchain installation. CI-based equivalents, where
|
||||
relevant, will rely on specific GitHub Actions instead of the Makefile.
|
||||
2. Go code generation will rely on `protoc` directly.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- We will still offer Go stub generation, proto linting and breakage checking.
|
||||
- Breakage checking will only happen on minor/patch releases to increase the
|
||||
signal-to-noise ratio in CI.
|
||||
- Versioned protos will be made available via Buf's registry upon every release.
|
||||
|
||||
### Negative
|
||||
|
||||
- Developers/contributors will need to install the relevant Protocol
|
||||
Buffers-related tooling (Buf, gogoprotobuf, clang-format) locally in order to
|
||||
build, lint, format and check `.proto` files for breaking changes.
|
||||
|
||||
### Neutral
|
||||
|
||||
## References
|
||||
|
||||
- [Protocol Buffers]
|
||||
- [Buf]
|
||||
- [\#7975]
|
||||
- [protoc] - The Protocol Buffers compiler
|
||||
|
||||
[Protocol Buffers]: https://developers.google.com/protocol-buffers
|
||||
[Buf]: https://buf.build/
|
||||
[\#7975]: https://github.com/tendermint/tendermint/pull/7975
|
||||
[protoc]: https://github.com/protocolbuffers/protobuf
|
||||
[clang-format]: https://clang.llvm.org/docs/ClangFormat.html
|
||||
[clang-format GitHub Action]: https://github.com/marketplace/actions/clang-format-github-action
|
||||
[buf-docker]: https://hub.docker.com/r/bufbuild/buf
|
||||
[cosmos-sdk-buf-registry-ci]: https://github.com/cosmos/cosmos-sdk/blob/e6571906043b6751951a42b6546431b1c38b05bd/.github/workflows/proto-registry.yml
|
||||
[cosmos-sdk-buf-linting-ci]: https://github.com/cosmos/cosmos-sdk/blob/e6571906043b6751951a42b6546431b1c38b05bd/.github/workflows/proto.yml#L15
|
||||
[\#8003]: https://github.com/tendermint/tendermint/issues/8003
|
||||
[Nix]: https://nixos.org/
|
||||
[gogoprotobuf]: https://github.com/gogo/protobuf
|
||||
[Prototool]: https://github.com/uber/prototool
|
||||
[buf-docs-gen]: https://docs.buf.build/bsr/documentation
|
||||
[redo]: https://redo.readthedocs.io/en/latest/
|
||||
@@ -6,12 +6,30 @@
|
||||
|
||||
## Status
|
||||
|
||||
> A decision may be "proposed" if it hasn't been agreed upon yet, or "accepted"
|
||||
> once it is agreed upon. Once the ADR has been implemented mark the ADR as
|
||||
> "implemented". If a later ADR changes or reverses a decision, it may be marked
|
||||
> as "deprecated" or "superseded" with a reference to its replacement.
|
||||
> An architecture decision is considered "proposed" when a PR containing the ADR
|
||||
> is submitted. When merged, an ADR must have a status associated with it, which
|
||||
> must be one of: "Accepted", "Rejected", "Deprecated" or "Superseded".
|
||||
>
|
||||
> An accepted ADR's implementation status must be tracked via a tracking issue,
|
||||
> milestone or project board (only one of these is necessary). For example:
|
||||
>
|
||||
> Accepted
|
||||
>
|
||||
> [Tracking issue](https://github.com/tendermint/tendermint/issues/123)
|
||||
> [Milestone](https://github.com/tendermint/tendermint/milestones/123)
|
||||
> [Project board](https://github.com/orgs/tendermint/projects/123)
|
||||
>
|
||||
> Rejected ADRs are captured as a record of recommendations that we specifically
|
||||
> do not (and possibly never) want to implement. The ADR itself must, for
|
||||
> posterity, include reasoning as to why it was rejected.
|
||||
>
|
||||
> If an ADR is deprecated, simply write "Deprecated" in this section. If an ADR
|
||||
> is superseded by one or more other ADRs, provide local a reference to those
|
||||
> ADRs, e.g.:
|
||||
>
|
||||
> Superseded by [ADR 123](./adr-123.md)
|
||||
|
||||
{Deprecated|Declined|Accepted|Implemented}
|
||||
Accepted | Rejected | Deprecated | Superseded by
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -594,7 +594,7 @@ This section will cover settings within the p2p section of the `config.toml`.
|
||||
- `pex` = turns the peer exchange reactor on or off. Validator node will want the `pex` turned off so it would not begin gossiping to unknown peers on the network. PeX can also be turned off for statically configured networks with fixed network connectivity. For full nodes on open, dynamic networks, it should be turned on.
|
||||
- `private-peer-ids` = is a comma-separated list of node ids that will _not_ be exposed to other peers (i.e., you will not tell other peers about the ids in this list). This can be filled with a validator's node id.
|
||||
|
||||
Recently the Tendermint Team conducted a refactor of the p2p layer. This lead to multiple config paramters being deprecated and/or replaced.
|
||||
Recently the Tendermint Team conducted a refactor of the p2p layer. This lead to multiple config parameters being deprecated and/or replaced.
|
||||
|
||||
We will cover the new and deprecated parameters below.
|
||||
### New Parameters
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# RFC 015: ABCI++ TX Mutation
|
||||
|
||||
## Changelog
|
||||
|
||||
- 23-Feb-2022: Initial draft (@williambanfield).
|
||||
- 28-Feb-2022: Revised draft (@williambanfield).
|
||||
|
||||
## Abstract
|
||||
|
||||
A previous version of the ABCI++ specification detailed a mechanism for proposers to replace transactions
|
||||
in the proposed block. This scheme required the proposer to construct new transactions
|
||||
and mark these new transactions as replacing other removed transactions. The specification
|
||||
was ambiguous as to how the replacement may be communicated to peer nodes.
|
||||
This RFC discusses issues with this mechanism and possible solutions.
|
||||
|
||||
## Background
|
||||
|
||||
### What is the proposed change?
|
||||
|
||||
A previous version of the ABCI++ specification proposed mechanisms for adding, removing, and replacing
|
||||
transactions in a proposed block. To replace a transaction, the application running
|
||||
`ProcessProposal` could mark a transaction as replaced by other application-supplied
|
||||
transactions by returning a new transaction marked with the `ADDED` flag setting
|
||||
the `new_hashes` field of the removed transaction to contain the list of transaction hashes
|
||||
that replace it. In that previous specification for ABCI++, the full use of the
|
||||
`new_hashes` field is left somewhat ambiguous. At present, these hashes are not
|
||||
gossiped and are not eventually included in the block to signal replacement to
|
||||
other nodes. The specification did indicate that the transactions specified in
|
||||
the `new_hashes` field will be removed from the mempool but it's not clear how
|
||||
peer nodes will learn about them.
|
||||
|
||||
### What systems would be affected by adding transaction replacement?
|
||||
|
||||
The 'transaction' is a central building block of a Tendermint blockchain, so adding
|
||||
a mechanism for transaction replacement would require changes to many aspects of Tendermint.
|
||||
|
||||
The following is a rough list of the functionality that this mechanism would affect:
|
||||
|
||||
#### Transaction indexing
|
||||
|
||||
Tendermint's indexer stores transactions and transaction results using the hash of the executed
|
||||
transaction [as the key][tx-result-index] and the ABCI results and transaction bytes as the value.
|
||||
|
||||
To allow transaction replacement, the replaced transactions would need to stored as well in the
|
||||
indexer, likely as a mapping of original transaction to list of transaction hashes that replaced
|
||||
the original transaction.
|
||||
|
||||
#### Transaction inclusion proofs
|
||||
|
||||
The result of a transaction query includes a Merkle proof of the existence of the
|
||||
transaction in the block chain. This [proof is built][inclusion-proof] as a merkle tree
|
||||
of the hashes of all of the transactions in the block where the queried transaction was executed.
|
||||
|
||||
To allow transaction replacement, these proofs would need to be updated to prove
|
||||
that a replaced transaction was included by replacement in the block.
|
||||
|
||||
#### RPC-based transaction query parameters and results
|
||||
|
||||
Tendermint's RPC allows clients to retrieve information about transactions via the
|
||||
`/tx_search` and `/tx` RPC endpoints.
|
||||
|
||||
RPC query results containing replaced transactions would need to be updated to include
|
||||
information on replaced transactions, either by returning results for all of the replaced
|
||||
transactions, or by including a response with just the hashes of the replaced transactions
|
||||
which clients could proceed to query individually.
|
||||
|
||||
#### Mempool transaction removal
|
||||
|
||||
Additional logic would need to be added to the Tendermint mempool to clear out replaced
|
||||
transactions after each block is executed. Tendermint currently removes executed transactions
|
||||
from the mempool, so this would be a pretty straightforward change.
|
||||
|
||||
## Discussion
|
||||
|
||||
### What value may be added to Tendermint by introducing transaction replacement?
|
||||
|
||||
Transaction replacement would would enable applications to aggregate or disaggregate transactions.
|
||||
|
||||
For aggregation, a set of transactions that all related work, such as transferring
|
||||
tokens between the same two accounts, could be replaced with a single transaction,
|
||||
i.e. one that transfers a single sum from one account to the other.
|
||||
Applications that make frequent use of aggregation may be able to achieve a higher throughput.
|
||||
Aggregation would decrease the space occupied by a single client-submitted transaction in the block, allowing
|
||||
more client-submitted transactions to be executed per block.
|
||||
|
||||
For disaggregation, a very complex transaction could be split into multiple smaller transactions.
|
||||
This may be useful if an application wishes to perform more fine-grained indexing on intermediate parts
|
||||
of a multi-part transaction.
|
||||
|
||||
### Drawbacks to transaction replacement
|
||||
|
||||
Transaction replacement would require updating and shimming many of the places that
|
||||
Tendermint records and exposes information about executed transactions. While
|
||||
systems within Tendermint could be updated to account for transaction replacement,
|
||||
such a system would leave new issues and rough edges.
|
||||
|
||||
#### No way of guaranteeing correct replacement
|
||||
|
||||
If a user issues a transaction to the network and the transaction is replaced, the
|
||||
user has no guarantee that the replacement was correct. For example, suppose a set of users issue
|
||||
transactions A, B, and C and they are all aggregated into a new transaction, D.
|
||||
There is nothing guaranteeing that D was constructed correctly from the inputs.
|
||||
The only way for users to ensure D is correct would be if D contained all of the
|
||||
information of its constituent transactions, in which case, nothing is really gained by the replacement.
|
||||
|
||||
#### Replacement transactions not signed by submitter
|
||||
|
||||
Abstractly, Tendermint simply views transactions as a ball of bytes and therefore
|
||||
should be fine with replacing one for another. However, many applications require
|
||||
that transactions submitted to the chain be signed by some private key to authenticate
|
||||
and authorize the transaction. Replaced transactions could not be signed by the
|
||||
submitter, only by the application node. Therefore, any use of transaction replacement
|
||||
could not contain authorization from the submitter and would either need to grant
|
||||
application-submitted transactions power to perform application logic on behalf
|
||||
of a user without their consent.
|
||||
|
||||
Granting this power to application-submitted transactions would be very dangerous
|
||||
and therefore might not be of much value to application developers.
|
||||
Transaction replacement might only be really safe in the case of application-submitted
|
||||
transactions or for transactions that require no authorization. For such transactions,
|
||||
it's quite not quite clear what the utility of replacement is: the application can already
|
||||
generate any transactions that it wants. The fact that such a transaction was a replacement
|
||||
is not particularly relevant to participants in the chain since the application is
|
||||
merely replacing its own transactions.
|
||||
|
||||
#### New vector for censorship
|
||||
|
||||
Depending on the implementation, transaction replacement may allow a node signal
|
||||
to the rest of the chain that some transaction should no longer be considered for execution.
|
||||
Honest nodes will use the replacement mechanism to signal that a transaction has been aggregated.
|
||||
Malicious nodes will be granted a new vector for censoring transactions.
|
||||
There is no guarantee that a replaced transactions is actually executed at all.
|
||||
A malicious node could censor a transaction by simply listing it as replaced.
|
||||
Honest nodes seeing the replacement would flush the transaction from their mempool
|
||||
and not execute or propose it it in later blocks.
|
||||
|
||||
### Transaction tracking implementations
|
||||
|
||||
This section discusses possible ways to flesh out the implementation of transaction replacement.
|
||||
Specifically, this section proposes a few alternative ways that Tendermint blockchains could
|
||||
track and store transaction replacements.
|
||||
|
||||
#### Include transaction replacements in the block
|
||||
|
||||
One option to track transaction replacement is to include information on the
|
||||
transaction replacement within the block. An additional structure may be added
|
||||
the block of the following form:
|
||||
|
||||
```proto
|
||||
message Block {
|
||||
...
|
||||
repeated Replacement replacements = 5;
|
||||
}
|
||||
|
||||
message Replacement {
|
||||
bytes included_tx_key = 1;
|
||||
repeated bytes replaced_txs_keys = 2;
|
||||
}
|
||||
```
|
||||
|
||||
Applications executing `PrepareProposal` would return the list of replacements and
|
||||
Tendermint would include an encoding of these replacements in the block that is gossiped
|
||||
and committed.
|
||||
|
||||
Tendermint's transaction indexing would include a new mapping for each replaced transaction
|
||||
key to the committed transaction.
|
||||
Transaction inclusion proofs would be updated to include these additional new transaction
|
||||
keys in the Merkle tree and queries for transaction hashes that were replaced would return
|
||||
information indicating that the transaction was replaced along with the hash of the
|
||||
transaction that replaced it.
|
||||
|
||||
Block validation of gossiped blocks would be updated to check that each of the
|
||||
`included_txs_key` matches the hash of some transaction in the proposed block.
|
||||
|
||||
Implementing the changes described in this section would allow Tendermint to gossip
|
||||
and index transaction replacements as part of block propagation. These changes would
|
||||
still require the application to certify that the replacements were valid. This
|
||||
validation may be performed in one of two ways:
|
||||
|
||||
1. **Applications optimistically trust that the proposer performed a legitimate replacement.**
|
||||
|
||||
In this validation scheme, applications would not verify that the substitution
|
||||
is valid during consensus and instead simply trust that the proposer is correct.
|
||||
This would have the drawback of allowing a malicious proposer to remove transactions
|
||||
it did not want executed.
|
||||
|
||||
2. **Applications completely validate transaction replacement.**
|
||||
|
||||
In this validation scheme, applications that allow replacement would check that
|
||||
each listed replaced transaction was correctly reflected in the replacement transaction.
|
||||
In order to perform such validation, the node would need to have the replaced transactions
|
||||
locally. This could be accomplished one of a few ways: by querying the mempool,
|
||||
by adding an additional p2p gossip channel for transaction replacements, or by including the replaced transactions
|
||||
in the block. Replacement validation via mempool querying would require the node
|
||||
to have received all of the replaced transactions in the mempool which is far from
|
||||
guaranteed. Adding an additional gossip channel would make gossiping replaced transactions
|
||||
a requirement for consensus to proceed, since all nodes would need to receive all replacement
|
||||
messages before considering a block valid. Finally, including replaced transactions in
|
||||
the block seems to obviate any benefit gained from performing a transaction replacement
|
||||
since the replaced transaction and the original transactions would now both appear in the block.
|
||||
|
||||
#### Application defined transaction replacement
|
||||
|
||||
An additional option for allowing transaction replacement is to leave it entirely as a responsibility
|
||||
of the application. The `PrepareProposal` ABCI++ call allows for applications to add
|
||||
new transactions to a proposed block. Applications that wished to implement a transaction
|
||||
replacement mechanism would be free to do so without the newly defined `new_hashes` field.
|
||||
Applications wishing to implement transaction replacement would add the aggregated
|
||||
transactions in the `PrepareProposal` response, and include one additional bookkeeping
|
||||
transaction that listed all of the replacements, with a similar scheme to the `new_hashes`
|
||||
field described in ABCI++. This new bookkeeping transaction could be used by the
|
||||
application to determine which transactions to clear from the mempool in future calls
|
||||
to `CheckTx`.
|
||||
|
||||
The meaning of any transaction in the block is completely opaque to Tendermint,
|
||||
so applications performing this style of replacement would not be able to have the replacement
|
||||
reflected in any most of Tendermint's transaction tracking mechanisms, such as transaction indexing
|
||||
and the `/tx` endpoint.
|
||||
|
||||
#### Application defined Tx Keys
|
||||
|
||||
Tendermint currently uses cryptographic hashes, SHA256, as a key for each transaction.
|
||||
As noted in the section on systems that would require changing, this key is used
|
||||
to identify the transaction in the mempool, in the indexer, and within the RPC system.
|
||||
|
||||
An alternative approach to allowing `ProcessProposal` to specify a set of transaction
|
||||
replacements would be instead to allow the application to specify an additional key or set
|
||||
of keys for each transaction during `ProcessProposal`. This new `secondary_keys` set
|
||||
would be included in the block and therefore gossiped during block propagation.
|
||||
Additional RPC endpoints could be exposed to query by the application-defined keys.
|
||||
|
||||
Applications wishing to implement replacement would leverage this new field by providing the
|
||||
replaced transaction hashes as the `secondary_keys` and checking their validity during
|
||||
`ProcessProposal`. During `RecheckTx` the application would then be responsible for
|
||||
clearing out transactions that matched the `secondary_keys`.
|
||||
|
||||
It is worth noting that something like this would be possible without `secondary_keys`.
|
||||
An application wishing to implement a system like this one could define a replacement
|
||||
transaction, as discussed in the section on application-defined transaction replacement,
|
||||
and use a custom [ABCI event type][abci-event-type] to communicate that the replacement should
|
||||
be indexed within Tendermint's ABCI event indexing.
|
||||
|
||||
### Complexity to value-add tradeoff
|
||||
|
||||
It is worth remarking that adding a system like this may introduce a decent amount
|
||||
of new complexity into Tendermint. An approach that leaves much of the replacement
|
||||
logic to Tendermint would require altering the core transaction indexing and querying
|
||||
data. In many of the cases listed, a system for transaction replacement is possible
|
||||
without explicitly defining it as part of `PrepareProposal`. Since applications
|
||||
can now add transactions during `PrepareProposal` they can and should leverage this
|
||||
functionality to include additional bookkeeping transactions in the block. It may
|
||||
be worth encouraging applications to discover new and interesting ways to leverage this
|
||||
power instead of immediately solving the problem for them.
|
||||
|
||||
### References
|
||||
|
||||
[inclusion-proof]: https://github.com/tendermint/tendermint/blob/0fcfaa4568cb700e27c954389c1fcd0b9e786332/types/tx.go#L67
|
||||
[tx-serach-result]: https://github.com/tendermint/tendermint/blob/0fcfaa4568cb700e27c954389c1fcd0b9e786332/rpc/coretypes/responses.go#L267
|
||||
[tx-rpc-func]: https://github.com/tendermint/tendermint/blob/0fcfaa4568cb700e27c954389c1fcd0b9e786332/internal/rpc/core/tx.go#L21
|
||||
[tx-result-index]: https://github.com/tendermint/tendermint/blob/0fcfaa4568cb700e27c954389c1fcd0b9e786332/internal/state/indexer/tx/kv/kv.go#L90
|
||||
[abci-event-type]: https://github.com/tendermint/tendermint/blob/0fcfaa4568cb700e27c954389c1fcd0b9e786332/abci/types/types.pb.go#L3168
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
order: 3
|
||||
---
|
||||
|
||||
# PBTS
|
||||
|
||||
This document provides an overview of the Proposer-Based Timestamp (PBTS)
|
||||
algorithm added to Tendermint in the v0.36 release. It outlines the core
|
||||
functionality as well as the parameters and constraints of the this algorithm.
|
||||
|
||||
## Algorithm Overview
|
||||
|
||||
The PBTS algorithm defines a way for a Tendermint blockchain to create block
|
||||
timestamps that are within a reasonable bound of the clocks of the validators on
|
||||
the network. This replaces the original BFTTime algorithm for timestamp
|
||||
assignment that relied on the timestamps included in precommit messages.
|
||||
|
||||
## Algorithm Parameters
|
||||
|
||||
The functionality of the PBTS algorithm is governed by two parameters within
|
||||
Tendermint. These two parameters are [consensus
|
||||
parameters](https://github.com/tendermint/tendermint/blob/master/spec/abci/apps.md#L291),
|
||||
meaning they are configured by the ABCI application and are expected to be the
|
||||
same across all nodes on the network.
|
||||
|
||||
### `Precision`
|
||||
|
||||
The `Precision` parameter configures the acceptable upper-bound of clock drift
|
||||
among all of the nodes on a Tendermint network. Any two nodes on a Tendermint
|
||||
network are expected to have clocks that differ by at most `Precision`
|
||||
milliseconds any given instant.
|
||||
|
||||
### `MessageDelay`
|
||||
|
||||
The `MessageDelay` parameter configures the acceptable upper-bound for
|
||||
transmitting a `Proposal` message from the proposer to _all_ of the validators
|
||||
on the network.
|
||||
|
||||
Networks should choose as small a value for `MessageDelay` as is practical,
|
||||
provided it is large enough that messages can reach all participants with high
|
||||
probability given the number of participants and latency of their connections.
|
||||
|
||||
## Algorithm Concepts
|
||||
|
||||
### Block timestamps
|
||||
|
||||
Each block produced by the Tendermint consensus engine contains a timestamp.
|
||||
The timestamp produced in each block is a meaningful representation of time that is
|
||||
useful for the protocols and applications built on top of Tendermint.
|
||||
|
||||
The following protocols and application features require a reliable source of time:
|
||||
|
||||
* Tendermint Light Clients [rely on correspondence between their known time](https://github.com/tendermint/tendermint/blob/master/spec/light-client/verification/README.md#definitions-1) and the block time for block verification.
|
||||
* Tendermint Evidence validity is determined [either in terms of heights or in terms of time](https://github.com/tendermint/tendermint/blob/master/spec/consensus/evidence.md#verification).
|
||||
* Unbonding of staked assets in the Cosmos Hub [occurs after a period of 21
|
||||
days](https://github.com/cosmos/governance/blob/master/params-change/Staking.md#unbondingtime).
|
||||
* IBC packets can use either a [timestamp or a height to timeout packet
|
||||
delivery](https://docs.cosmos.network/v0.44/ibc/overview.html#acknowledgements)
|
||||
|
||||
### Proposer Selects a Block Timestamp
|
||||
|
||||
When the proposer node creates a new block proposal, the node reads the time
|
||||
from its local clock and uses this reading as the timestamp for the proposed
|
||||
block.
|
||||
|
||||
### Timeliness
|
||||
|
||||
When each validator on a Tendermint network receives a proposed block, it
|
||||
performs a series of checks to ensure that the block can be considered valid as
|
||||
a candidate to be the next block in the chain.
|
||||
|
||||
The PBTS algorithm performs a validity check on the timestamp of proposed
|
||||
blocks. When a validator receives a proposal it ensures that the timestamp in
|
||||
the proposal is within a bound of the validator's local clock. Specifically, the
|
||||
algorithm checks that the timestamp is no more than `Precision` greater than the
|
||||
node's local clock and no less than `Precision` + `MessageDelay` behind than the
|
||||
node's local clock. This creates range of acceptable timestamps around the
|
||||
node's local time. If the timestamp is within this range, the PBTS algorithm
|
||||
considers the block **timely**. If a block is not **timely**, the node will
|
||||
issue a `nil` `prevote` for this block, signaling to the rest of the network
|
||||
that the node does not consider the block to be valid.
|
||||
|
||||
### Clock Synchronization
|
||||
|
||||
The PBTS algorithm requires the clocks of the validators on a Tendermint network
|
||||
are within `Precision` of each other. In practice, this means that validators
|
||||
should periodically synchronize to a reliable NTP server. Validators that drift
|
||||
too far away from the rest of the network will no longer propose blocks with
|
||||
valid timestamps. Additionally they will not view the timestamps of blocks
|
||||
proposed by their peers to be valid either.
|
||||
|
||||
## See Also
|
||||
|
||||
* [The PBTS specification](https://github.com/tendermint/tendermint/blob/master/spec/consensus/proposer-based-timestamp/README.md)
|
||||
contains all of the details of the algorithm.
|
||||
@@ -1,3 +1,4 @@
|
||||
master master
|
||||
v0.33.x v0.33
|
||||
v0.34.x v0.34
|
||||
v0.35.x v0.35
|
||||
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/rs/cors v1.8.2
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tendermint/tm-db v0.6.6
|
||||
@@ -34,11 +34,17 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
|
||||
golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
google.golang.org/grpc v1.44.0
|
||||
google.golang.org/grpc v1.45.0
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
pgregory.net/rapid v0.4.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/creachadair/atomicfile v0.2.4
|
||||
github.com/google/go-cmp v0.5.7
|
||||
gotest.tools v2.2.0+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||
github.com/Antonboom/errname v0.1.5 // indirect
|
||||
@@ -67,7 +73,6 @@ require (
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/containerd/continuity v0.2.1 // indirect
|
||||
github.com/daixiang0/gci v0.3.1-0.20220208004058-76d765e3ab48 // indirect
|
||||
github.com/creachadair/atomicfile v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.2 // indirect
|
||||
@@ -107,7 +112,6 @@ require (
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
||||
|
||||
@@ -940,8 +940,9 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
|
||||
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
|
||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@@ -1625,8 +1626,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
|
||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
||||
@@ -168,7 +168,7 @@ func (pool *BlockPool) removeTimedoutPeers() {
|
||||
for _, peer := range pool.peers {
|
||||
// check if peer timed out
|
||||
if !peer.didTimeout && peer.numPending > 0 {
|
||||
curRate := peer.recvMonitor.Status().CurRate
|
||||
curRate := peer.recvMonitor.CurrentTransferRate()
|
||||
// curRate can be 0 on start
|
||||
if curRate != 0 && curRate < minRecvRate {
|
||||
err := errors.New("peer is not sending us data fast enough")
|
||||
|
||||
@@ -70,6 +70,8 @@ type Reactor struct {
|
||||
|
||||
// immutable
|
||||
initialState sm.State
|
||||
// store
|
||||
stateStore sm.Store
|
||||
|
||||
blockExec *sm.BlockExecutor
|
||||
store *store.BlockStore
|
||||
@@ -101,7 +103,7 @@ type Reactor struct {
|
||||
func NewReactor(
|
||||
ctx context.Context,
|
||||
logger log.Logger,
|
||||
state sm.State,
|
||||
stateStore sm.Store,
|
||||
blockExec *sm.BlockExecutor,
|
||||
store *store.BlockStore,
|
||||
consReactor consensusReactor,
|
||||
@@ -111,19 +113,6 @@ func NewReactor(
|
||||
metrics *consensus.Metrics,
|
||||
eventBus *eventbus.EventBus,
|
||||
) (*Reactor, error) {
|
||||
|
||||
if state.LastBlockHeight != store.Height() {
|
||||
return nil, fmt.Errorf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, store.Height())
|
||||
}
|
||||
|
||||
startHeight := store.Height() + 1
|
||||
if startHeight == 1 {
|
||||
startHeight = state.InitialHeight
|
||||
}
|
||||
|
||||
requestsCh := make(chan BlockRequest, maxTotalRequesters)
|
||||
errorsCh := make(chan peerError, maxPeerErrBuffer) // NOTE: The capacity should be larger than the peer count.
|
||||
|
||||
blockSyncCh, err := channelCreator(ctx, GetChannelDescriptor())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -131,20 +120,16 @@ func NewReactor(
|
||||
|
||||
r := &Reactor{
|
||||
logger: logger,
|
||||
initialState: state,
|
||||
stateStore: stateStore,
|
||||
blockExec: blockExec,
|
||||
store: store,
|
||||
pool: NewBlockPool(logger, startHeight, requestsCh, errorsCh),
|
||||
consReactor: consReactor,
|
||||
blockSync: newAtomicBool(blockSync),
|
||||
requestsCh: requestsCh,
|
||||
errorsCh: errorsCh,
|
||||
blockSyncCh: blockSyncCh,
|
||||
blockSyncOutBridgeCh: make(chan p2p.Envelope),
|
||||
peerUpdates: peerUpdates,
|
||||
metrics: metrics,
|
||||
eventBus: eventBus,
|
||||
syncStartTime: time.Time{},
|
||||
}
|
||||
|
||||
r.BaseService = *service.NewBaseService(logger, "BlockSync", r)
|
||||
@@ -159,6 +144,27 @@ func NewReactor(
|
||||
// If blockSync is enabled, we also start the pool and the pool processing
|
||||
// goroutine. If the pool fails to start, an error is returned.
|
||||
func (r *Reactor) OnStart(ctx context.Context) error {
|
||||
state, err := r.stateStore.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.initialState = state
|
||||
|
||||
if state.LastBlockHeight != r.store.Height() {
|
||||
return fmt.Errorf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, r.store.Height())
|
||||
}
|
||||
|
||||
startHeight := r.store.Height() + 1
|
||||
if startHeight == 1 {
|
||||
startHeight = state.InitialHeight
|
||||
}
|
||||
|
||||
requestsCh := make(chan BlockRequest, maxTotalRequesters)
|
||||
errorsCh := make(chan peerError, maxPeerErrBuffer) // NOTE: The capacity should be larger than the peer count.
|
||||
r.pool = NewBlockPool(r.logger, startHeight, requestsCh, errorsCh)
|
||||
r.requestsCh = requestsCh
|
||||
r.errorsCh = errorsCh
|
||||
|
||||
if r.blockSync.IsSet() {
|
||||
if err := r.pool.Start(ctx); err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fortytw2/leaktest"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
@@ -14,7 +15,8 @@ import (
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/consensus"
|
||||
"github.com/tendermint/tendermint/internal/mempool/mock"
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
mpmocks "github.com/tendermint/tendermint/internal/mempool/mocks"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/p2ptest"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
@@ -33,7 +35,7 @@ type reactorTestSuite struct {
|
||||
nodes []types.NodeID
|
||||
|
||||
reactors map[types.NodeID]*Reactor
|
||||
app map[types.NodeID]proxy.AppConns
|
||||
app map[types.NodeID]abciclient.Client
|
||||
|
||||
blockSyncChannels map[types.NodeID]*p2p.Channel
|
||||
peerChans map[types.NodeID]chan p2p.PeerUpdate
|
||||
@@ -64,7 +66,7 @@ func setup(
|
||||
network: p2ptest.MakeNetwork(ctx, t, p2ptest.NetworkOptions{NumNodes: numNodes}),
|
||||
nodes: make([]types.NodeID, 0, numNodes),
|
||||
reactors: make(map[types.NodeID]*Reactor, numNodes),
|
||||
app: make(map[types.NodeID]proxy.AppConns, numNodes),
|
||||
app: make(map[types.NodeID]abciclient.Client, numNodes),
|
||||
blockSyncChannels: make(map[types.NodeID]*p2p.Channel, numNodes),
|
||||
peerChans: make(map[types.NodeID]chan p2p.PeerUpdate, numNodes),
|
||||
peerUpdates: make(map[types.NodeID]*p2p.PeerUpdates, numNodes),
|
||||
@@ -109,7 +111,7 @@ func (rts *reactorTestSuite) addNode(
|
||||
logger := log.TestingLogger()
|
||||
|
||||
rts.nodes = append(rts.nodes, nodeID)
|
||||
rts.app[nodeID] = proxy.NewAppConns(abciclient.NewLocalCreator(&abci.BaseApplication{}), logger, proxy.NopMetrics())
|
||||
rts.app[nodeID] = proxy.New(abciclient.NewLocalClient(logger, &abci.BaseApplication{}), logger, proxy.NopMetrics())
|
||||
require.NoError(t, rts.app[nodeID].Start(ctx))
|
||||
|
||||
blockDB := dbm.NewMemDB()
|
||||
@@ -120,14 +122,29 @@ func (rts *reactorTestSuite) addNode(
|
||||
state, err := sm.MakeGenesisState(genDoc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, stateStore.Save(state))
|
||||
mp := &mpmocks.Mempool{}
|
||||
mp.On("Lock").Return()
|
||||
mp.On("Unlock").Return()
|
||||
mp.On("FlushAppConn", mock.Anything).Return(nil)
|
||||
mp.On("Update",
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything).Return(nil)
|
||||
|
||||
eventbus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventbus.Start(ctx))
|
||||
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
rts.app[nodeID].Consensus(),
|
||||
mock.Mempool{},
|
||||
rts.app[nodeID],
|
||||
mp,
|
||||
sm.EmptyEvidencePool{},
|
||||
blockStore,
|
||||
eventbus,
|
||||
)
|
||||
|
||||
for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ {
|
||||
@@ -154,8 +171,7 @@ func (rts *reactorTestSuite) addNode(
|
||||
)
|
||||
}
|
||||
|
||||
thisBlock, err := sf.MakeBlock(state, blockHeight, lastCommit)
|
||||
require.NoError(t, err)
|
||||
thisBlock := sf.MakeBlock(state, blockHeight, lastCommit)
|
||||
thisParts, err := thisBlock.MakePartSet(types.BlockPartSizeBytes)
|
||||
require.NoError(t, err)
|
||||
blockID := types.BlockID{Hash: thisBlock.Hash(), PartSetHeader: thisParts.Header()}
|
||||
@@ -176,7 +192,7 @@ func (rts *reactorTestSuite) addNode(
|
||||
rts.reactors[nodeID], err = NewReactor(
|
||||
ctx,
|
||||
rts.logger.With("nodeID", nodeID),
|
||||
state.Copy(),
|
||||
stateStore,
|
||||
blockExec,
|
||||
blockStore,
|
||||
nil,
|
||||
|
||||
@@ -82,37 +82,33 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
log.TestingLogger().With("module", "mempool"),
|
||||
thisConfig.Mempool,
|
||||
proxyAppConnMem,
|
||||
0,
|
||||
)
|
||||
if thisConfig.Consensus.WaitForTxs() {
|
||||
mempool.EnableTxsAvailable()
|
||||
}
|
||||
|
||||
eventBus := eventbus.NewDefault(log.TestingLogger().With("module", "events"))
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
// Make a full instance of the evidence pool
|
||||
evidenceDB := dbm.NewMemDB()
|
||||
evpool, err := evidence.NewPool(logger.With("module", "evidence"), evidenceDB, stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
evpool := evidence.NewPool(logger.With("module", "evidence"), evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
// Make State
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore)
|
||||
cs := NewState(ctx, logger, thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool)
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore, eventBus)
|
||||
cs, err := NewState(ctx, logger, thisConfig.Consensus, stateStore, blockExec, blockStore, mempool, evpool, eventBus)
|
||||
require.NoError(t, err)
|
||||
// set private validator
|
||||
pv := privVals[i]
|
||||
cs.SetPrivValidator(ctx, pv)
|
||||
|
||||
eventBus := eventbus.NewDefault(log.TestingLogger().With("module", "events"))
|
||||
err = eventBus.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
cs.SetEventBus(eventBus)
|
||||
evpool.SetEventBus(eventBus)
|
||||
|
||||
cs.SetTimeoutTicker(tickerFunc())
|
||||
|
||||
states[i] = cs
|
||||
}()
|
||||
}
|
||||
|
||||
rts := setup(ctx, t, nValidators, states, 100) // buffer must be large enough to not deadlock
|
||||
rts := setup(ctx, t, nValidators, states, 512) // buffer must be large enough to not deadlock
|
||||
|
||||
var bzNodeID types.NodeID
|
||||
|
||||
@@ -180,7 +176,6 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
require.NotNil(t, lazyNodeState.privValidator)
|
||||
|
||||
var commit *types.Commit
|
||||
var votes []*types.Vote
|
||||
switch {
|
||||
case lazyNodeState.Height == lazyNodeState.state.InitialHeight:
|
||||
// We're creating a proposal for the first block.
|
||||
@@ -189,7 +184,6 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
case lazyNodeState.LastCommit.HasTwoThirdsMajority():
|
||||
// Make the commit from LastCommit
|
||||
commit = lazyNodeState.LastCommit.MakeCommit()
|
||||
votes = lazyNodeState.LastCommit.GetVotes()
|
||||
default: // This shouldn't happen.
|
||||
lazyNodeState.logger.Error("enterPropose: Cannot propose anything: No commit for the previous block")
|
||||
return
|
||||
@@ -206,9 +200,10 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
}
|
||||
proposerAddr := lazyNodeState.privValidatorPubKey.Address()
|
||||
|
||||
block, blockParts, err := lazyNodeState.blockExec.CreateProposalBlock(
|
||||
ctx, lazyNodeState.Height, lazyNodeState.state, commit, proposerAddr, votes,
|
||||
)
|
||||
block, err := lazyNodeState.blockExec.CreateProposalBlock(
|
||||
ctx, lazyNodeState.Height, lazyNodeState.state, commit, proposerAddr, nil)
|
||||
require.NoError(t, err)
|
||||
blockParts, err := block.MakePartSet(types.BlockPartSizeBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Flush the WAL. Otherwise, we may not recompute the same proposal to sign,
|
||||
@@ -238,8 +233,7 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, reactor := range rts.reactors {
|
||||
state := reactor.state.GetState()
|
||||
reactor.SwitchToConsensus(ctx, state, false)
|
||||
reactor.SwitchToConsensus(ctx, reactor.state.GetState(), false)
|
||||
}
|
||||
|
||||
// Evidence should be submitted and committed at the third height but
|
||||
@@ -248,20 +242,26 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
i := 0
|
||||
subctx, subcancel := context.WithCancel(ctx)
|
||||
defer subcancel()
|
||||
for _, sub := range rts.subs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(j int, s eventbus.Subscription) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
if subctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := s.Next(subctx)
|
||||
if subctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := s.Next(ctx)
|
||||
assert.NoError(t, err)
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Errorf("waiting for subscription: %v", err)
|
||||
subcancel()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -273,12 +273,18 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}(i, sub)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// don't run more assertions if we've encountered a timeout
|
||||
select {
|
||||
case <-subctx.Done():
|
||||
t.Fatal("encountered timeout")
|
||||
default:
|
||||
}
|
||||
|
||||
pubkey, err := bzNodeState.privValidator.GetPubKey(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -290,267 +296,3 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
assert.Equal(t, prevoteHeight, ev.Height())
|
||||
}
|
||||
}
|
||||
|
||||
// 4 validators. 1 is byzantine. The other three are partitioned into A (1 val) and B (2 vals).
|
||||
// byzantine validator sends conflicting proposals into A and B,
|
||||
// and prevotes/precommits on both of them.
|
||||
// B sees a commit, A doesn't.
|
||||
// Heal partition and ensure A sees the commit
|
||||
func TestByzantineConflictingProposalsWithPartition(t *testing.T) {
|
||||
// TODO: https://github.com/tendermint/tendermint/issues/6092
|
||||
t.SkipNow()
|
||||
|
||||
// n := 4
|
||||
// logger := consensusLogger().With("test", "byzantine")
|
||||
// app := newCounter
|
||||
|
||||
// states, cleanup := randConsensusState(n, "consensus_byzantine_test", newMockTickerFunc(false), app)
|
||||
// t.Cleanup(cleanup)
|
||||
|
||||
// // give the byzantine validator a normal ticker
|
||||
// ticker := NewTimeoutTicker()
|
||||
// ticker.SetLogger(states[0].logger)
|
||||
// states[0].SetTimeoutTicker(ticker)
|
||||
|
||||
// p2pLogger := logger.With("module", "p2p")
|
||||
|
||||
// blocksSubs := make([]types.Subscription, n)
|
||||
// reactors := make([]p2p.Reactor, n)
|
||||
// for i := 0; i < n; i++ {
|
||||
// // enable txs so we can create different proposals
|
||||
// assertMempool(states[i].txNotifier).EnableTxsAvailable()
|
||||
|
||||
// eventBus := states[i].eventBus
|
||||
// eventBus.SetLogger(logger.With("module", "events", "validator", i))
|
||||
|
||||
// var err error
|
||||
// blocksSubs[i], err = eventBus.Subscribe(ctx, testSubscriber, types.EventQueryNewBlock)
|
||||
// require.NoError(t, err)
|
||||
|
||||
// conR := NewReactor(states[i], true) // so we don't start the consensus states
|
||||
// conR.SetLogger(logger.With("validator", i))
|
||||
// conR.SetEventBus(eventBus)
|
||||
|
||||
// var conRI p2p.Reactor = conR
|
||||
|
||||
// // make first val byzantine
|
||||
// if i == 0 {
|
||||
// conRI = NewByzantineReactor(conR)
|
||||
// }
|
||||
|
||||
// reactors[i] = conRI
|
||||
// err = states[i].blockExec.Store().Save(states[i].state) // for save height 1's validators info
|
||||
// require.NoError(t, err)
|
||||
// }
|
||||
|
||||
// switches := p2p.MakeConnectedSwitches(config.P2P, N, func(i int, sw *p2p.Switch) *p2p.Switch {
|
||||
// sw.SetLogger(p2pLogger.With("validator", i))
|
||||
// sw.AddReactor("CONSENSUS", reactors[i])
|
||||
// return sw
|
||||
// }, func(sws []*p2p.Switch, i, j int) {
|
||||
// // the network starts partitioned with globally active adversary
|
||||
// if i != 0 {
|
||||
// return
|
||||
// }
|
||||
// p2p.Connect2Switches(sws, i, j)
|
||||
// })
|
||||
|
||||
// // make first val byzantine
|
||||
// // NOTE: Now, test validators are MockPV, which by default doesn't
|
||||
// // do any safety checks.
|
||||
// states[0].privValidator.(types.MockPV).DisableChecks()
|
||||
// states[0].decideProposal = func(j int32) func(int64, int32) {
|
||||
// return func(height int64, round int32) {
|
||||
// byzantineDecideProposalFunc(t, height, round, states[j], switches[j])
|
||||
// }
|
||||
// }(int32(0))
|
||||
// // We are setting the prevote function to do nothing because the prevoting
|
||||
// // and precommitting are done alongside the proposal.
|
||||
// states[0].doPrevote = func(height int64, round int32) {}
|
||||
|
||||
// defer func() {
|
||||
// for _, sw := range switches {
|
||||
// err := sw.Stop()
|
||||
// require.NoError(t, err)
|
||||
// }
|
||||
// }()
|
||||
|
||||
// // start the non-byz state machines.
|
||||
// // note these must be started before the byz
|
||||
// for i := 1; i < n; i++ {
|
||||
// cr := reactors[i].(*Reactor)
|
||||
// cr.SwitchToConsensus(cr.conS.GetState(), false)
|
||||
// }
|
||||
|
||||
// // start the byzantine state machine
|
||||
// byzR := reactors[0].(*ByzantineReactor)
|
||||
// s := byzR.reactor.conS.GetState()
|
||||
// byzR.reactor.SwitchToConsensus(s, false)
|
||||
|
||||
// // byz proposer sends one block to peers[0]
|
||||
// // and the other block to peers[1] and peers[2].
|
||||
// // note peers and switches order don't match.
|
||||
// peers := switches[0].Peers().List()
|
||||
|
||||
// // partition A
|
||||
// ind0 := getSwitchIndex(switches, peers[0])
|
||||
|
||||
// // partition B
|
||||
// ind1 := getSwitchIndex(switches, peers[1])
|
||||
// ind2 := getSwitchIndex(switches, peers[2])
|
||||
// p2p.Connect2Switches(switches, ind1, ind2)
|
||||
|
||||
// // wait for someone in the big partition (B) to make a block
|
||||
// <-blocksSubs[ind2].Out()
|
||||
|
||||
// t.Log("A block has been committed. Healing partition")
|
||||
// p2p.Connect2Switches(switches, ind0, ind1)
|
||||
// p2p.Connect2Switches(switches, ind0, ind2)
|
||||
|
||||
// // wait till everyone makes the first new block
|
||||
// // (one of them already has)
|
||||
// wg := new(sync.WaitGroup)
|
||||
// for i := 1; i < N-1; i++ {
|
||||
// wg.Add(1)
|
||||
// go func(j int) {
|
||||
// <-blocksSubs[j].Out()
|
||||
// wg.Done()
|
||||
// }(i)
|
||||
// }
|
||||
|
||||
// done := make(chan struct{})
|
||||
// go func() {
|
||||
// wg.Wait()
|
||||
// close(done)
|
||||
// }()
|
||||
|
||||
// tick := time.NewTicker(time.Second * 10)
|
||||
// select {
|
||||
// case <-done:
|
||||
// case <-tick.C:
|
||||
// for i, reactor := range reactors {
|
||||
// t.Log(fmt.Sprintf("Consensus Reactor %v", i))
|
||||
// t.Log(fmt.Sprintf("%v", reactor))
|
||||
// }
|
||||
// t.Fatalf("Timed out waiting for all validators to commit first block")
|
||||
// }
|
||||
}
|
||||
|
||||
// func byzantineDecideProposalFunc(t *testing.T, height int64, round int32, cs *State, sw *p2p.Switch) {
|
||||
// // byzantine user should create two proposals and try to split the vote.
|
||||
// // Avoid sending on internalMsgQueue and running consensus state.
|
||||
|
||||
// // Create a new proposal block from state/txs from the mempool.
|
||||
// block1, blockParts1 := cs.createProposalBlock()
|
||||
// polRound, propBlockID := cs.ValidRound, types.BlockID{Hash: block1.Hash(), PartSetHeader: blockParts1.Header()}
|
||||
// proposal1 := types.NewProposal(height, round, polRound, propBlockID)
|
||||
// p1 := proposal1.ToProto()
|
||||
// if err := cs.privValidator.SignProposal(cs.state.ChainID, p1); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// proposal1.Signature = p1.Signature
|
||||
|
||||
// // some new transactions come in (this ensures that the proposals are different)
|
||||
// deliverTxsRange(cs, 0, 1)
|
||||
|
||||
// // Create a new proposal block from state/txs from the mempool.
|
||||
// block2, blockParts2 := cs.createProposalBlock()
|
||||
// polRound, propBlockID = cs.ValidRound, types.BlockID{Hash: block2.Hash(), PartSetHeader: blockParts2.Header()}
|
||||
// proposal2 := types.NewProposal(height, round, polRound, propBlockID)
|
||||
// p2 := proposal2.ToProto()
|
||||
// if err := cs.privValidator.SignProposal(cs.state.ChainID, p2); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// proposal2.Signature = p2.Signature
|
||||
|
||||
// block1Hash := block1.Hash()
|
||||
// block2Hash := block2.Hash()
|
||||
|
||||
// // broadcast conflicting proposals/block parts to peers
|
||||
// peers := sw.Peers().List()
|
||||
// t.Logf("Byzantine: broadcasting conflicting proposals to %d peers", len(peers))
|
||||
// for i, peer := range peers {
|
||||
// if i < len(peers)/2 {
|
||||
// go sendProposalAndParts(height, round, cs, peer, proposal1, block1Hash, blockParts1)
|
||||
// } else {
|
||||
// go sendProposalAndParts(height, round, cs, peer, proposal2, block2Hash, blockParts2)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func sendProposalAndParts(
|
||||
// height int64,
|
||||
// round int32,
|
||||
// cs *State,
|
||||
// peer p2p.Peer,
|
||||
// proposal *types.Proposal,
|
||||
// blockHash []byte,
|
||||
// parts *types.PartSet,
|
||||
// ) {
|
||||
// // proposal
|
||||
// msg := &ProposalMessage{Proposal: proposal}
|
||||
// peer.Send(DataChannel, MustEncode(msg))
|
||||
|
||||
// // parts
|
||||
// for i := 0; i < int(parts.Total()); i++ {
|
||||
// part := parts.GetPart(i)
|
||||
// msg := &BlockPartMessage{
|
||||
// Height: height, // This tells peer that this part applies to us.
|
||||
// Round: round, // This tells peer that this part applies to us.
|
||||
// Part: part,
|
||||
// }
|
||||
// peer.Send(DataChannel, MustEncode(msg))
|
||||
// }
|
||||
|
||||
// // votes
|
||||
// cs.mtx.Lock()
|
||||
// prevote, _ := cs.signVote(tmproto.PrevoteType, blockHash, parts.Header())
|
||||
// precommit, _ := cs.signVote(tmproto.PrecommitType, blockHash, parts.Header())
|
||||
// cs.mtx.Unlock()
|
||||
|
||||
// peer.Send(VoteChannel, MustEncode(&VoteMessage{prevote}))
|
||||
// peer.Send(VoteChannel, MustEncode(&VoteMessage{precommit}))
|
||||
// }
|
||||
|
||||
// type ByzantineReactor struct {
|
||||
// service.Service
|
||||
// reactor *Reactor
|
||||
// }
|
||||
|
||||
// func NewByzantineReactor(conR *Reactor) *ByzantineReactor {
|
||||
// return &ByzantineReactor{
|
||||
// Service: conR,
|
||||
// reactor: conR,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (br *ByzantineReactor) SetSwitch(s *p2p.Switch) { br.reactor.SetSwitch(s) }
|
||||
// func (br *ByzantineReactor) GetChannels() []*p2p.ChannelDescriptor { return br.reactor.GetChannels() }
|
||||
|
||||
// func (br *ByzantineReactor) AddPeer(peer p2p.Peer) {
|
||||
// if !br.reactor.IsRunning() {
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Create peerState for peer
|
||||
// peerState := NewPeerState(peer).SetLogger(br.reactor.logger)
|
||||
// peer.Set(types.PeerStateKey, peerState)
|
||||
|
||||
// // Send our state to peer.
|
||||
// // If we're syncing, broadcast a RoundStepMessage later upon SwitchToConsensus().
|
||||
// if !br.reactor.waitSync {
|
||||
// br.reactor.sendNewRoundStepMessage(peer)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (br *ByzantineReactor) RemovePeer(peer p2p.Peer, reason interface{}) {
|
||||
// br.reactor.RemovePeer(peer, reason)
|
||||
// }
|
||||
|
||||
// func (br *ByzantineReactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) {
|
||||
// br.reactor.Receive(chID, peer, msgBytes)
|
||||
// }
|
||||
|
||||
// func (br *ByzantineReactor) InitPeer(peer p2p.Peer) p2p.Peer { return peer }
|
||||
|
||||
@@ -69,6 +69,9 @@ func configSetup(t *testing.T) *config.Config {
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(configByzantineTest.RootDir) })
|
||||
|
||||
walDir := filepath.Dir(cfg.Consensus.WalFile())
|
||||
ensureDir(t, walDir, 0700)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -239,7 +242,9 @@ func decideProposal(
|
||||
t.Helper()
|
||||
|
||||
cs1.mtx.Lock()
|
||||
block, blockParts, err := cs1.createProposalBlock(ctx)
|
||||
block, err := cs1.createProposalBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
blockParts, err := block.MakePartSet(types.BlockPartSizeBytes)
|
||||
require.NoError(t, err)
|
||||
validRound := cs1.ValidRound
|
||||
chainID := cs1.state.ChainID
|
||||
@@ -370,7 +375,11 @@ func subscribeToVoter(ctx context.Context, t *testing.T, cs *State, addr []byte)
|
||||
vote := msg.Data().(types.EventDataVote)
|
||||
// we only fire for our own votes
|
||||
if bytes.Equal(addr, vote.Vote.ValidatorAddress) {
|
||||
ch <- msg
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case ch <- msg:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, types.EventQueryVote); err != nil {
|
||||
@@ -401,7 +410,10 @@ func subscribeToVoterBuffered(ctx context.Context, t *testing.T, cs *State, addr
|
||||
vote := msg.Data().(types.EventDataVote)
|
||||
// we only fire for our own votes
|
||||
if bytes.Equal(addr, vote.Vote.ValidatorAddress) {
|
||||
ch <- msg
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case ch <- msg:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -462,7 +474,6 @@ func newStateWithConfigAndBlockStore(
|
||||
logger.With("module", "mempool"),
|
||||
thisConfig.Mempool,
|
||||
proxyAppConnMem,
|
||||
0,
|
||||
)
|
||||
|
||||
if thisConfig.Consensus.WaitForTxs() {
|
||||
@@ -476,22 +487,26 @@ func newStateWithConfigAndBlockStore(
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
require.NoError(t, stateStore.Save(state))
|
||||
|
||||
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyAppConnCon, mempool, evpool, blockStore)
|
||||
cs := NewState(ctx,
|
||||
eventBus := eventbus.NewDefault(logger.With("module", "events"))
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyAppConnCon, mempool, evpool, blockStore, eventBus)
|
||||
cs, err := NewState(ctx,
|
||||
logger.With("module", "consensus"),
|
||||
thisConfig.Consensus,
|
||||
state,
|
||||
stateStore,
|
||||
blockExec,
|
||||
blockStore,
|
||||
mempool,
|
||||
evpool,
|
||||
eventBus,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cs.SetPrivValidator(ctx, pv)
|
||||
|
||||
eventBus := eventbus.NewDefault(logger.With("module", "events"))
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
cs.SetEventBus(eventBus)
|
||||
return cs
|
||||
}
|
||||
|
||||
@@ -775,6 +790,7 @@ func makeConsensusState(
|
||||
configOpts ...func(*config.Config),
|
||||
) ([]*State, cleanupFunc) {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
valSet, privVals := factory.ValidatorSet(ctx, t, nValidators, 30)
|
||||
genDoc := factory.GenesisDoc(cfg, time.Now(), valSet.Validators, nil)
|
||||
@@ -789,7 +805,7 @@ func makeConsensusState(
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB()) // each state needs its own db
|
||||
state, err := sm.MakeGenesisState(genDoc)
|
||||
require.NoError(t, err)
|
||||
thisConfig, err := ResetConfig(t.TempDir(), fmt.Sprintf("%s_%d", testName, i))
|
||||
thisConfig, err := ResetConfig(tempDir, fmt.Sprintf("%s_%d", testName, i))
|
||||
require.NoError(t, err)
|
||||
|
||||
configRootDirs = append(configRootDirs, thisConfig.RootDir)
|
||||
@@ -798,7 +814,8 @@ func makeConsensusState(
|
||||
opt(thisConfig)
|
||||
}
|
||||
|
||||
ensureDir(t, filepath.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal
|
||||
walDir := filepath.Dir(thisConfig.Consensus.WalFile())
|
||||
ensureDir(t, walDir, 0700)
|
||||
|
||||
app := kvstore.NewApplication()
|
||||
closeFuncs = append(closeFuncs, app.Close)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func TestReactorInvalidPrecommit(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
config := configSetup(t)
|
||||
@@ -49,14 +50,14 @@ func TestReactorInvalidPrecommit(t *testing.T) {
|
||||
byzState := rts.states[node.NodeID]
|
||||
byzReactor := rts.reactors[node.NodeID]
|
||||
|
||||
calledDoPrevote := false
|
||||
signal := make(chan struct{})
|
||||
// Update the doPrevote function to just send a valid precommit for a random
|
||||
// block and otherwise disable the priv validator.
|
||||
byzState.mtx.Lock()
|
||||
privVal := byzState.privValidator
|
||||
byzState.doPrevote = func(ctx context.Context, height int64, round int32) {
|
||||
defer close(signal)
|
||||
invalidDoPrevoteFunc(ctx, t, height, round, byzState, byzReactor, privVal)
|
||||
calledDoPrevote = true
|
||||
}
|
||||
byzState.mtx.Unlock()
|
||||
|
||||
@@ -72,16 +73,30 @@ func TestReactorInvalidPrecommit(t *testing.T) {
|
||||
go func(s eventbus.Subscription) {
|
||||
defer wg.Done()
|
||||
_, err := s.Next(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if !assert.NoError(t, err) {
|
||||
cancel() // cancel other subscribers on failure
|
||||
}
|
||||
}(sub)
|
||||
}
|
||||
}
|
||||
wait := make(chan struct{})
|
||||
go func() { defer close(wait); wg.Wait() }()
|
||||
|
||||
wg.Wait()
|
||||
if !calledDoPrevote {
|
||||
t.Fatal("test failed to run core logic")
|
||||
select {
|
||||
case <-wait:
|
||||
if _, ok := <-signal; !ok {
|
||||
t.Fatal("test condition did not fire")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if _, ok := <-signal; !ok {
|
||||
t.Fatal("test condition did not fire after timeout")
|
||||
return
|
||||
}
|
||||
case <-signal:
|
||||
// test passed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,19 +145,27 @@ func invalidDoPrevoteFunc(
|
||||
cs.privValidator = nil // disable priv val so we don't do normal votes
|
||||
cs.mtx.Unlock()
|
||||
|
||||
count := 0
|
||||
r.mtx.Lock()
|
||||
ids := make([]types.NodeID, 0, len(r.peers))
|
||||
for _, ps := range r.peers {
|
||||
ids = append(ids, ps.peerID)
|
||||
}
|
||||
r.mtx.Unlock()
|
||||
|
||||
count := 0
|
||||
for _, peerID := range ids {
|
||||
count++
|
||||
err := r.voteCh.Send(ctx, p2p.Envelope{
|
||||
To: ps.peerID,
|
||||
To: peerID,
|
||||
Message: &tmcons.Vote{
|
||||
Vote: precommit.ToProto(),
|
||||
},
|
||||
})
|
||||
// we want to have sent some of these votes,
|
||||
// but if the test completes without erroring
|
||||
// and we get here, we shouldn't error
|
||||
if errors.Is(err, context.Canceled) && count > 1 {
|
||||
// or not sending any messages, then we should
|
||||
// error.
|
||||
if errors.Is(err, context.Canceled) && count > 0 {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) {
|
||||
|
||||
ensureNewEventOnChannel(t, newBlockCh) // first block gets committed
|
||||
ensureNoNewEventOnChannel(t, newBlockCh)
|
||||
deliverTxsRange(ctx, t, cs, 0, 1)
|
||||
checkTxsRange(ctx, t, cs, 0, 1)
|
||||
ensureNewEventOnChannel(t, newBlockCh) // commit txs
|
||||
ensureNewEventOnChannel(t, newBlockCh) // commit updated app hash
|
||||
ensureNoNewEventOnChannel(t, newBlockCh)
|
||||
@@ -118,7 +118,7 @@ func TestMempoolProgressInHigherRound(t *testing.T) {
|
||||
round = 0
|
||||
|
||||
ensureNewRound(t, newRoundCh, height, round) // first round at next height
|
||||
deliverTxsRange(ctx, t, cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round
|
||||
checkTxsRange(ctx, t, cs, 0, 1) // we deliver txs, but don't set a proposal so we get the next round
|
||||
ensureNewTimeout(t, timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds())
|
||||
|
||||
round++ // moving to the next round
|
||||
@@ -126,7 +126,7 @@ func TestMempoolProgressInHigherRound(t *testing.T) {
|
||||
ensureNewEventOnChannel(t, newBlockCh) // now we can commit the block
|
||||
}
|
||||
|
||||
func deliverTxsRange(ctx context.Context, t *testing.T, cs *State, start, end int) {
|
||||
func checkTxsRange(ctx context.Context, t *testing.T, cs *State, start, end int) {
|
||||
t.Helper()
|
||||
// Deliver some txs.
|
||||
for i := start; i < end; i++ {
|
||||
@@ -159,7 +159,7 @@ func TestMempoolTxConcurrentWithCommit(t *testing.T) {
|
||||
newBlockHeaderCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlockHeader)
|
||||
|
||||
const numTxs int64 = 3000
|
||||
go deliverTxsRange(ctx, t, cs, 0, int(numTxs))
|
||||
go checkTxsRange(ctx, t, cs, 0, int(numTxs))
|
||||
|
||||
startTestRound(ctx, cs, cs.Height, cs.Round)
|
||||
for n := int64(0); n < numTxs; {
|
||||
@@ -192,8 +192,8 @@ func TestMempoolRmBadTx(t *testing.T) {
|
||||
txBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(txBytes, uint64(0))
|
||||
|
||||
resDeliver := app.FinalizeBlock(abci.RequestFinalizeBlock{Txs: [][]byte{txBytes}})
|
||||
assert.False(t, resDeliver.Txs[0].IsErr(), fmt.Sprintf("expected no error. got %v", resDeliver))
|
||||
resFinalize := app.FinalizeBlock(abci.RequestFinalizeBlock{Txs: [][]byte{txBytes}})
|
||||
assert.False(t, resFinalize.TxResults[0].IsErr(), fmt.Sprintf("expected no error. got %v", resFinalize))
|
||||
|
||||
resCommit := app.Commit()
|
||||
assert.True(t, len(resCommit.Data) > 0)
|
||||
@@ -212,7 +212,7 @@ func TestMempoolRmBadTx(t *testing.T) {
|
||||
checkTxRespCh <- struct{}{}
|
||||
}, mempool.TxInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("error after CheckTx: %w", err)
|
||||
t.Errorf("error after CheckTx: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -265,20 +265,20 @@ func (app *CounterApplication) Info(req abci.RequestInfo) abci.ResponseInfo {
|
||||
}
|
||||
|
||||
func (app *CounterApplication) FinalizeBlock(req abci.RequestFinalizeBlock) abci.ResponseFinalizeBlock {
|
||||
respTxs := make([]*abci.ResponseDeliverTx, len(req.Txs))
|
||||
respTxs := make([]*abci.ExecTxResult, len(req.Txs))
|
||||
for i, tx := range req.Txs {
|
||||
txValue := txAsUint64(tx)
|
||||
if txValue != uint64(app.txCount) {
|
||||
respTxs[i] = &abci.ResponseDeliverTx{
|
||||
respTxs[i] = &abci.ExecTxResult{
|
||||
Code: code.CodeTypeBadNonce,
|
||||
Log: fmt.Sprintf("Invalid nonce. Expected %d, got %d", app.txCount, txValue),
|
||||
}
|
||||
continue
|
||||
}
|
||||
app.txCount++
|
||||
respTxs[i] = &abci.ResponseDeliverTx{Code: code.CodeTypeOK}
|
||||
respTxs[i] = &abci.ExecTxResult{Code: code.CodeTypeOK}
|
||||
}
|
||||
return abci.ResponseFinalizeBlock{Txs: respTxs}
|
||||
return abci.ResponseFinalizeBlock{TxResults: respTxs}
|
||||
}
|
||||
|
||||
func (app *CounterApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
|
||||
@@ -310,7 +310,7 @@ func (app *CounterApplication) Commit() abci.ResponseCommit {
|
||||
|
||||
func (app *CounterApplication) PrepareProposal(
|
||||
req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
|
||||
return abci.ResponsePrepareProposal{BlockData: req.BlockData}
|
||||
return abci.ResponsePrepareProposal{}
|
||||
}
|
||||
|
||||
func (app *CounterApplication) ProcessProposal(
|
||||
|
||||
@@ -203,7 +203,7 @@ func (p *pbtsTestHarness) nextHeight(ctx context.Context, t *testing.T, proposer
|
||||
|
||||
ensureNewRound(t, p.roundCh, p.currentHeight, p.currentRound)
|
||||
|
||||
b, _, err := p.observedState.createProposalBlock(ctx)
|
||||
b, err := p.observedState.createProposalBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
b.Height = p.currentHeight
|
||||
b.Header.Height = p.currentHeight
|
||||
|
||||
@@ -138,6 +138,7 @@ func NewReactor(
|
||||
cs *State,
|
||||
channelCreator p2p.ChannelCreator,
|
||||
peerUpdates *p2p.PeerUpdates,
|
||||
eventBus *eventbus.EventBus,
|
||||
waitSync bool,
|
||||
metrics *Metrics,
|
||||
) (*Reactor, error) {
|
||||
@@ -166,6 +167,7 @@ func NewReactor(
|
||||
state: cs,
|
||||
waitSync: waitSync,
|
||||
peers: make(map[types.NodeID]*PeerState),
|
||||
eventBus: eventBus,
|
||||
Metrics: metrics,
|
||||
stateCh: stateCh,
|
||||
dataCh: dataCh,
|
||||
@@ -226,12 +228,6 @@ func (r *Reactor) OnStop() {
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventBus sets the reactor's event bus.
|
||||
func (r *Reactor) SetEventBus(b *eventbus.EventBus) {
|
||||
r.eventBus = b
|
||||
r.state.SetEventBus(b)
|
||||
}
|
||||
|
||||
// WaitSync returns whether the consensus reactor is waiting for state/block sync.
|
||||
func (r *Reactor) WaitSync() bool {
|
||||
r.mtx.RLock()
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -110,13 +109,12 @@ func setup(
|
||||
state,
|
||||
chCreator(nodeID),
|
||||
node.MakePeerUpdates(ctx, t),
|
||||
state.eventBus,
|
||||
true,
|
||||
NopMetrics(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
reactor.SetEventBus(state.eventBus)
|
||||
|
||||
blocksSub, err := state.eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
|
||||
ClientID: testSubscriber,
|
||||
Query: types.EventQueryNewBlock,
|
||||
@@ -461,12 +459,12 @@ func TestReactorWithEvidence(t *testing.T) {
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
state, err := sm.MakeGenesisState(genDoc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, stateStore.Save(state))
|
||||
thisConfig, err := ResetConfig(t.TempDir(), fmt.Sprintf("%s_%d", testName, i))
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.RemoveAll(thisConfig.RootDir)
|
||||
|
||||
ensureDir(t, path.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal
|
||||
app := kvstore.NewApplication()
|
||||
vals := types.TM2PB.ValidatorUpdates(state.Validators)
|
||||
app.InitChain(abci.RequestInitChain{Validators: vals})
|
||||
@@ -483,7 +481,6 @@ func TestReactorWithEvidence(t *testing.T) {
|
||||
log.TestingLogger().With("module", "mempool"),
|
||||
thisConfig.Mempool,
|
||||
proxyAppConnMem,
|
||||
0,
|
||||
)
|
||||
|
||||
if thisConfig.Consensus.WaitForTxs() {
|
||||
@@ -504,15 +501,15 @@ func TestReactorWithEvidence(t *testing.T) {
|
||||
|
||||
evpool2 := sm.EmptyEvidencePool{}
|
||||
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore)
|
||||
|
||||
cs := NewState(ctx, logger.With("validator", i, "module", "consensus"),
|
||||
thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool2)
|
||||
cs.SetPrivValidator(ctx, pv)
|
||||
|
||||
eventBus := eventbus.NewDefault(log.TestingLogger().With("module", "events"))
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
cs.SetEventBus(eventBus)
|
||||
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore, eventBus)
|
||||
|
||||
cs, err := NewState(ctx, logger.With("validator", i, "module", "consensus"),
|
||||
thisConfig.Consensus, stateStore, blockExec, blockStore, mempool, evpool2, eventBus)
|
||||
require.NoError(t, err)
|
||||
cs.SetPrivValidator(ctx, pv)
|
||||
|
||||
cs.SetTimeoutTicker(tickerFunc())
|
||||
|
||||
@@ -565,7 +562,6 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) {
|
||||
c.Consensus.CreateEmptyBlocks = false
|
||||
},
|
||||
)
|
||||
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
rts := setup(ctx, t, n, states, 100) // buffer must be large enough to not deadlock
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
@@ -204,7 +205,7 @@ type Handshaker struct {
|
||||
stateStore sm.Store
|
||||
initialState sm.State
|
||||
store sm.BlockStore
|
||||
eventBus types.BlockEventPublisher
|
||||
eventBus *eventbus.EventBus
|
||||
genDoc *types.GenesisDoc
|
||||
logger log.Logger
|
||||
|
||||
@@ -216,7 +217,7 @@ func NewHandshaker(
|
||||
stateStore sm.Store,
|
||||
state sm.State,
|
||||
store sm.BlockStore,
|
||||
eventBus types.BlockEventPublisher,
|
||||
eventBus *eventbus.EventBus,
|
||||
genDoc *types.GenesisDoc,
|
||||
) *Handshaker {
|
||||
|
||||
@@ -237,10 +238,10 @@ func (h *Handshaker) NBlocks() int {
|
||||
}
|
||||
|
||||
// TODO: retry the handshake/replay if it fails ?
|
||||
func (h *Handshaker) Handshake(ctx context.Context, proxyApp proxy.AppConns) error {
|
||||
func (h *Handshaker) Handshake(ctx context.Context, appClient abciclient.Client) error {
|
||||
|
||||
// Handshake is done via ABCI Info on the query conn.
|
||||
res, err := proxyApp.Query().Info(ctx, proxy.RequestInfo)
|
||||
res, err := appClient.Info(ctx, proxy.RequestInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling Info: %w", err)
|
||||
}
|
||||
@@ -264,7 +265,7 @@ func (h *Handshaker) Handshake(ctx context.Context, proxyApp proxy.AppConns) err
|
||||
}
|
||||
|
||||
// Replay blocks up to the latest in the blockstore.
|
||||
_, err = h.ReplayBlocks(ctx, h.initialState, appHash, blockHeight, proxyApp)
|
||||
_, err = h.ReplayBlocks(ctx, h.initialState, appHash, blockHeight, appClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on replay: %w", err)
|
||||
}
|
||||
@@ -285,7 +286,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
state sm.State,
|
||||
appHash []byte,
|
||||
appBlockHeight int64,
|
||||
proxyApp proxy.AppConns,
|
||||
appClient abciclient.Client,
|
||||
) ([]byte, error) {
|
||||
storeBlockBase := h.store.Base()
|
||||
storeBlockHeight := h.store.Height()
|
||||
@@ -316,7 +317,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
Validators: nextVals,
|
||||
AppStateBytes: h.genDoc.AppState,
|
||||
}
|
||||
res, err := proxyApp.Consensus().InitChain(ctx, req)
|
||||
res, err := appClient.InitChain(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -390,7 +391,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
// Either the app is asking for replay, or we're all synced up.
|
||||
if appBlockHeight < storeBlockHeight {
|
||||
// the app is behind, so replay blocks, but no need to go through WAL (state is already synced to store)
|
||||
return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, false)
|
||||
return h.replayBlocks(ctx, state, appClient, appBlockHeight, storeBlockHeight, false)
|
||||
|
||||
} else if appBlockHeight == storeBlockHeight {
|
||||
// We're good!
|
||||
@@ -405,7 +406,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
case appBlockHeight < stateBlockHeight:
|
||||
// the app is further behind than it should be, so replay blocks
|
||||
// but leave the last block to go through the WAL
|
||||
return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, true)
|
||||
return h.replayBlocks(ctx, state, appClient, appBlockHeight, storeBlockHeight, true)
|
||||
|
||||
case appBlockHeight == stateBlockHeight:
|
||||
// We haven't run Commit (both the state and app are one block behind),
|
||||
@@ -413,7 +414,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
// NOTE: We could instead use the cs.WAL on cs.Start,
|
||||
// but we'd have to allow the WAL to replay a block that wrote it's #ENDHEIGHT
|
||||
h.logger.Info("Replay last block using real app")
|
||||
state, err = h.replayBlock(ctx, state, storeBlockHeight, proxyApp.Consensus())
|
||||
state, err = h.replayBlock(ctx, state, storeBlockHeight, appClient)
|
||||
return state.AppHash, err
|
||||
|
||||
case appBlockHeight == storeBlockHeight:
|
||||
@@ -426,6 +427,9 @@ func (h *Handshaker) ReplayBlocks(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mockApp.Start(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.logger.Info("Replay last block using mock app")
|
||||
state, err = h.replayBlock(ctx, state, storeBlockHeight, mockApp)
|
||||
@@ -445,7 +449,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
func (h *Handshaker) replayBlocks(
|
||||
ctx context.Context,
|
||||
state sm.State,
|
||||
proxyApp proxy.AppConns,
|
||||
appClient abciclient.Client,
|
||||
appBlockHeight,
|
||||
storeBlockHeight int64,
|
||||
mutateState bool) ([]byte, error) {
|
||||
@@ -480,17 +484,15 @@ func (h *Handshaker) replayBlocks(
|
||||
if i == finalBlock && !mutateState {
|
||||
// We emit events for the index services at the final block due to the sync issue when
|
||||
// the node shutdown during the block committing status.
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
h.stateStore, h.logger, proxyApp.Consensus(), emptyMempool{}, sm.EmptyEvidencePool{}, h.store)
|
||||
blockExec.SetEventBus(h.eventBus)
|
||||
blockExec := sm.NewBlockExecutor(h.stateStore, h.logger, appClient, emptyMempool{}, sm.EmptyEvidencePool{}, h.store, h.eventBus)
|
||||
appHash, err = sm.ExecCommitBlock(ctx,
|
||||
blockExec, proxyApp.Consensus(), block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
|
||||
blockExec, appClient, block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
appHash, err = sm.ExecCommitBlock(ctx,
|
||||
nil, proxyApp.Consensus(), block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
|
||||
nil, appClient, block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -501,7 +503,7 @@ func (h *Handshaker) replayBlocks(
|
||||
|
||||
if mutateState {
|
||||
// sync the final block
|
||||
state, err = h.replayBlock(ctx, state, storeBlockHeight, proxyApp.Consensus())
|
||||
state, err = h.replayBlock(ctx, state, storeBlockHeight, appClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -517,15 +519,14 @@ func (h *Handshaker) replayBlock(
|
||||
ctx context.Context,
|
||||
state sm.State,
|
||||
height int64,
|
||||
proxyApp proxy.AppConnConsensus,
|
||||
appClient abciclient.Client,
|
||||
) (sm.State, error) {
|
||||
block := h.store.LoadBlock(height)
|
||||
meta := h.store.LoadBlockMeta(height)
|
||||
|
||||
// Use stubs for both mempool and evidence pool since no transactions nor
|
||||
// evidence are needed here - block already exists.
|
||||
blockExec := sm.NewBlockExecutor(h.stateStore, h.logger, proxyApp, emptyMempool{}, sm.EmptyEvidencePool{}, h.store)
|
||||
blockExec.SetEventBus(h.eventBus)
|
||||
blockExec := sm.NewBlockExecutor(h.stateStore, h.logger, appClient, emptyMempool{}, sm.EmptyEvidencePool{}, h.store, h.eventBus)
|
||||
|
||||
var err error
|
||||
state, err = blockExec.ApplyBlock(ctx, state, meta.BlockID, block)
|
||||
|
||||
@@ -84,7 +84,7 @@ func (cs *State) ReplayFile(ctx context.Context, file string, console bool) erro
|
||||
return err
|
||||
}
|
||||
|
||||
pb := newPlayback(file, fp, cs, cs.state.Copy())
|
||||
pb := newPlayback(file, fp, cs, cs.stateStore)
|
||||
defer pb.fp.Close()
|
||||
|
||||
var nextN int // apply N msgs in a row
|
||||
@@ -126,17 +126,17 @@ type playback struct {
|
||||
count int // how many lines/msgs into the file are we
|
||||
|
||||
// replays can be reset to beginning
|
||||
fileName string // so we can close/reopen the file
|
||||
genesisState sm.State // so the replay session knows where to restart from
|
||||
fileName string // so we can close/reopen the file
|
||||
stateStore sm.Store
|
||||
}
|
||||
|
||||
func newPlayback(fileName string, fp *os.File, cs *State, genState sm.State) *playback {
|
||||
func newPlayback(fileName string, fp *os.File, cs *State, store sm.Store) *playback {
|
||||
return &playback{
|
||||
cs: cs,
|
||||
fp: fp,
|
||||
fileName: fileName,
|
||||
genesisState: genState,
|
||||
dec: NewWALDecoder(fp),
|
||||
cs: cs,
|
||||
fp: fp,
|
||||
fileName: fileName,
|
||||
stateStore: store,
|
||||
dec: NewWALDecoder(fp),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,11 @@ func (pb *playback) replayReset(ctx context.Context, count int, newStepSub event
|
||||
pb.cs.Stop()
|
||||
pb.cs.Wait()
|
||||
|
||||
newCS := NewState(ctx, pb.cs.logger, pb.cs.config, pb.genesisState.Copy(), pb.cs.blockExec,
|
||||
pb.cs.blockStore, pb.cs.txNotifier, pb.cs.evpool)
|
||||
newCS.SetEventBus(pb.cs.eventBus)
|
||||
newCS, err := NewState(ctx, pb.cs.logger, pb.cs.config, pb.stateStore, pb.cs.blockExec,
|
||||
pb.cs.blockStore, pb.cs.txNotifier, pb.cs.evpool, pb.cs.eventBus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newCS.startForReplay()
|
||||
|
||||
if err := pb.fp.Close(); err != nil {
|
||||
@@ -323,9 +325,12 @@ func newConsensusStateForReplay(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create proxyAppConn connection (consensus, mempool, query)
|
||||
clientCreator, _ := proxy.DefaultClientCreator(logger, cfg.ProxyApp, cfg.ABCI, cfg.DBDir())
|
||||
proxyApp := proxy.NewAppConns(clientCreator, logger, proxy.NopMetrics())
|
||||
client, _, err := proxy.ClientFactory(logger, cfg.ProxyApp, cfg.ABCI, cfg.DBDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxyApp := proxy.New(client, logger, proxy.NopMetrics())
|
||||
err = proxyApp.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting proxy app conns: %w", err)
|
||||
@@ -343,11 +348,12 @@ func newConsensusStateForReplay(
|
||||
}
|
||||
|
||||
mempool, evpool := emptyMempool{}, sm.EmptyEvidencePool{}
|
||||
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyApp.Consensus(), mempool, evpool, blockStore)
|
||||
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyApp, mempool, evpool, blockStore, eventBus)
|
||||
|
||||
consensusState := NewState(ctx, logger, csConfig, state.Copy(), blockExec,
|
||||
blockStore, mempool, evpool)
|
||||
|
||||
consensusState.SetEventBus(eventBus)
|
||||
consensusState, err := NewState(ctx, logger, csConfig, stateStore, blockExec,
|
||||
blockStore, mempool, evpool, eventBus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return consensusState, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (emptyMempool) Update(
|
||||
_ context.Context,
|
||||
_ int64,
|
||||
_ types.Txs,
|
||||
_ []*abci.ResponseDeliverTx,
|
||||
_ []*abci.ExecTxResult,
|
||||
_ mempool.PreCheckFunc,
|
||||
_ mempool.PostCheckFunc,
|
||||
) error {
|
||||
@@ -61,22 +61,11 @@ func newMockProxyApp(
|
||||
logger log.Logger,
|
||||
appHash []byte,
|
||||
abciResponses *tmstate.ABCIResponses,
|
||||
) (proxy.AppConnConsensus, error) {
|
||||
|
||||
clientCreator := abciclient.NewLocalCreator(&mockProxyApp{
|
||||
) (abciclient.Client, error) {
|
||||
return proxy.New(abciclient.NewLocalClient(logger, &mockProxyApp{
|
||||
appHash: appHash,
|
||||
abciResponses: abciResponses,
|
||||
})
|
||||
cli, err := clientCreator(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = cli.Start(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return proxy.NewAppConnConsensus(cli, proxy.NopMetrics()), nil
|
||||
}), logger, proxy.NopMetrics()), nil
|
||||
}
|
||||
|
||||
type mockProxyApp struct {
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
@@ -381,7 +380,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
|
||||
newValidatorTx1 := kvstore.MakeValSetChangeTx(valPubKey1ABCI, testMinPower)
|
||||
err = assertMempool(t, css[0].txNotifier).CheckTx(ctx, newValidatorTx1, nil, mempool.TxInfo{})
|
||||
assert.NoError(t, err)
|
||||
propBlock, _, err := css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
propBlock, err := css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
require.NoError(t, err)
|
||||
propBlockParts, err := propBlock.MakePartSet(partSize)
|
||||
require.NoError(t, err)
|
||||
@@ -415,7 +414,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
|
||||
updateValidatorTx1 := kvstore.MakeValSetChangeTx(updatePubKey1ABCI, 25)
|
||||
err = assertMempool(t, css[0].txNotifier).CheckTx(ctx, updateValidatorTx1, nil, mempool.TxInfo{})
|
||||
assert.NoError(t, err)
|
||||
propBlock, _, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
propBlock, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
require.NoError(t, err)
|
||||
propBlockParts, err = propBlock.MakePartSet(partSize)
|
||||
require.NoError(t, err)
|
||||
@@ -456,7 +455,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
|
||||
newValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, testMinPower)
|
||||
err = assertMempool(t, css[0].txNotifier).CheckTx(ctx, newValidatorTx3, nil, mempool.TxInfo{})
|
||||
assert.NoError(t, err)
|
||||
propBlock, _, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
propBlock, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
require.NoError(t, err)
|
||||
propBlockParts, err = propBlock.MakePartSet(partSize)
|
||||
require.NoError(t, err)
|
||||
@@ -544,7 +543,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
|
||||
removeValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, 0)
|
||||
err = assertMempool(t, css[0].txNotifier).CheckTx(ctx, removeValidatorTx3, nil, mempool.TxInfo{})
|
||||
assert.NoError(t, err)
|
||||
propBlock, _, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
propBlock, err = css[0].createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
require.NoError(t, err)
|
||||
propBlockParts, err = propBlock.MakePartSet(partSize)
|
||||
require.NoError(t, err)
|
||||
@@ -652,61 +651,6 @@ func TestHandshakeReplayNone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test mockProxyApp should not panic when app return ABCIResponses with some empty ResponseDeliverTx
|
||||
func TestMockProxyApp(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sim := setupSimulator(ctx, t) // setup config and simulator
|
||||
cfg := sim.Config
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
logger := log.TestingLogger()
|
||||
var validTxs, invalidTxs = 0, 0
|
||||
txCount := 0
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
abciResWithEmptyDeliverTx := new(tmstate.ABCIResponses)
|
||||
abciResWithEmptyDeliverTx.FinalizeBlock = new(abci.ResponseFinalizeBlock)
|
||||
abciResWithEmptyDeliverTx.FinalizeBlock.Txs = make([]*abci.ResponseDeliverTx, 0)
|
||||
abciResWithEmptyDeliverTx.FinalizeBlock.Txs = append(abciResWithEmptyDeliverTx.FinalizeBlock.Txs, &abci.ResponseDeliverTx{})
|
||||
|
||||
// called when saveABCIResponses:
|
||||
bytes, err := proto.Marshal(abciResWithEmptyDeliverTx)
|
||||
require.NoError(t, err)
|
||||
loadedAbciRes := new(tmstate.ABCIResponses)
|
||||
|
||||
// this also happens sm.LoadABCIResponses
|
||||
err = proto.Unmarshal(bytes, loadedAbciRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
mock, err := newMockProxyApp(ctx, logger, []byte("mock_hash"), loadedAbciRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
abciRes := new(tmstate.ABCIResponses)
|
||||
abciRes.FinalizeBlock = new(abci.ResponseFinalizeBlock)
|
||||
abciRes.FinalizeBlock.Txs = make([]*abci.ResponseDeliverTx, len(loadedAbciRes.FinalizeBlock.Txs))
|
||||
|
||||
someTx := []byte("tx")
|
||||
resp, err := mock.FinalizeBlock(ctx, abci.RequestFinalizeBlock{Txs: [][]byte{someTx}})
|
||||
require.NoError(t, err)
|
||||
// TODO: make use of res.Log
|
||||
// TODO: make use of this info
|
||||
// Blocks may include invalid txs.
|
||||
for _, tx := range resp.Txs {
|
||||
if tx.Code == abci.CodeTypeOK {
|
||||
validTxs++
|
||||
} else {
|
||||
invalidTxs++
|
||||
}
|
||||
txCount++
|
||||
}
|
||||
})
|
||||
require.Equal(t, 1, txCount)
|
||||
require.Equal(t, 1, validTxs)
|
||||
require.Zero(t, invalidTxs)
|
||||
}
|
||||
|
||||
func tempWALWithData(t *testing.T, data []byte) string {
|
||||
t.Helper()
|
||||
|
||||
@@ -804,16 +748,19 @@ func testHandshakeReplay(
|
||||
filepath.Join(cfg.DBDir(), fmt.Sprintf("replay_test_%d_%d_a_r%d", nBlocks, mode, rand.Int())))
|
||||
t.Cleanup(func() { require.NoError(t, kvstoreApp.Close()) })
|
||||
|
||||
clientCreator2 := abciclient.NewLocalCreator(kvstoreApp)
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
clientCreator2 := abciclient.NewLocalClient(logger, kvstoreApp)
|
||||
if nBlocks > 0 {
|
||||
// run nBlocks against a new client to build up the app state.
|
||||
// use a throwaway tendermint state
|
||||
proxyApp := proxy.NewAppConns(clientCreator2, logger, proxy.NopMetrics())
|
||||
proxyApp := proxy.New(clientCreator2, logger, proxy.NopMetrics())
|
||||
stateDB1 := dbm.NewMemDB()
|
||||
stateStore := sm.NewStore(stateDB1)
|
||||
err := stateStore.Save(genesisState)
|
||||
require.NoError(t, err)
|
||||
buildAppStateFromChain(ctx, t, proxyApp, stateStore, sim.Mempool, sim.Evpool, genesisState, chain, nBlocks, mode, store)
|
||||
buildAppStateFromChain(ctx, t, proxyApp, stateStore, sim.Mempool, sim.Evpool, genesisState, chain, eventBus, nBlocks, mode, store)
|
||||
}
|
||||
|
||||
// Prune block store if requested
|
||||
@@ -828,10 +775,11 @@ func testHandshakeReplay(
|
||||
// now start the app using the handshake - it should sync
|
||||
genDoc, err := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
|
||||
require.NoError(t, err)
|
||||
handshaker := NewHandshaker(logger, stateStore, state, store, eventbus.NopEventBus{}, genDoc)
|
||||
proxyApp := proxy.NewAppConns(clientCreator2, logger, proxy.NopMetrics())
|
||||
handshaker := NewHandshaker(logger, stateStore, state, store, eventBus, genDoc)
|
||||
proxyApp := proxy.New(clientCreator2, logger, proxy.NopMetrics())
|
||||
require.NoError(t, proxyApp.Start(ctx), "Error starting proxy app connections")
|
||||
|
||||
require.True(t, proxyApp.IsRunning())
|
||||
require.NotNil(t, proxyApp)
|
||||
t.Cleanup(func() { cancel(); proxyApp.Wait() })
|
||||
|
||||
err = handshaker.Handshake(ctx, proxyApp)
|
||||
@@ -842,7 +790,7 @@ func testHandshakeReplay(
|
||||
require.NoError(t, err, "Error on abci handshake")
|
||||
|
||||
// get the latest app hash from the app
|
||||
res, err := proxyApp.Query().Info(ctx, abci.RequestInfo{Version: ""})
|
||||
res, err := proxyApp.Info(ctx, abci.RequestInfo{Version: ""})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -875,11 +823,12 @@ func applyBlock(
|
||||
evpool sm.EvidencePool,
|
||||
st sm.State,
|
||||
blk *types.Block,
|
||||
proxyApp proxy.AppConns,
|
||||
appClient abciclient.Client,
|
||||
blockStore *mockBlockStore,
|
||||
eventBus *eventbus.EventBus,
|
||||
) sm.State {
|
||||
testPartSize := types.BlockPartSizeBytes
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool, blockStore)
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), appClient, mempool, evpool, blockStore, eventBus)
|
||||
|
||||
bps, err := blk.MakePartSet(testPartSize)
|
||||
require.NoError(t, err)
|
||||
@@ -892,23 +841,24 @@ func applyBlock(
|
||||
func buildAppStateFromChain(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
proxyApp proxy.AppConns,
|
||||
appClient abciclient.Client,
|
||||
stateStore sm.Store,
|
||||
mempool mempool.Mempool,
|
||||
evpool sm.EvidencePool,
|
||||
state sm.State,
|
||||
chain []*types.Block,
|
||||
eventBus *eventbus.EventBus,
|
||||
nBlocks int,
|
||||
mode uint,
|
||||
blockStore *mockBlockStore,
|
||||
) {
|
||||
t.Helper()
|
||||
// start a new app without handshake, play nBlocks blocks
|
||||
require.NoError(t, proxyApp.Start(ctx))
|
||||
require.NoError(t, appClient.Start(ctx))
|
||||
|
||||
state.Version.Consensus.App = kvstore.ProtocolVersion // simulate handshake, receive app version
|
||||
validators := types.TM2PB.ValidatorUpdates(state.Validators)
|
||||
_, err := proxyApp.Consensus().InitChain(ctx, abci.RequestInitChain{
|
||||
_, err := appClient.InitChain(ctx, abci.RequestInitChain{
|
||||
Validators: validators,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -919,18 +869,18 @@ func buildAppStateFromChain(
|
||||
case 0:
|
||||
for i := 0; i < nBlocks; i++ {
|
||||
block := chain[i]
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, appClient, blockStore, eventBus)
|
||||
}
|
||||
case 1, 2, 3:
|
||||
for i := 0; i < nBlocks-1; i++ {
|
||||
block := chain[i]
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, appClient, blockStore, eventBus)
|
||||
}
|
||||
|
||||
if mode == 2 || mode == 3 {
|
||||
// update the kvstore height and apphash
|
||||
// as if we ran commit but not
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, chain[nBlocks-1], proxyApp, blockStore)
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, chain[nBlocks-1], appClient, blockStore, eventBus)
|
||||
}
|
||||
default:
|
||||
require.Fail(t, "unknown mode %v", mode)
|
||||
@@ -958,37 +908,40 @@ func buildTMStateFromChain(
|
||||
kvstoreApp := kvstore.NewPersistentKVStoreApplication(logger,
|
||||
filepath.Join(cfg.DBDir(), fmt.Sprintf("replay_test_%d_%d_t", nBlocks, mode)))
|
||||
defer kvstoreApp.Close()
|
||||
clientCreator := abciclient.NewLocalCreator(kvstoreApp)
|
||||
client := abciclient.NewLocalClient(logger, kvstoreApp)
|
||||
|
||||
proxyApp := proxy.NewAppConns(clientCreator, logger, proxy.NopMetrics())
|
||||
proxyApp := proxy.New(client, logger, proxy.NopMetrics())
|
||||
require.NoError(t, proxyApp.Start(ctx))
|
||||
|
||||
state.Version.Consensus.App = kvstore.ProtocolVersion // simulate handshake, receive app version
|
||||
validators := types.TM2PB.ValidatorUpdates(state.Validators)
|
||||
_, err := proxyApp.Consensus().InitChain(ctx, abci.RequestInitChain{
|
||||
_, err := proxyApp.InitChain(ctx, abci.RequestInitChain{
|
||||
Validators: validators,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, stateStore.Save(state))
|
||||
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
switch mode {
|
||||
case 0:
|
||||
// sync right up
|
||||
for _, block := range chain {
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore, eventBus)
|
||||
}
|
||||
|
||||
case 1, 2, 3:
|
||||
// sync up to the penultimate as if we stored the block.
|
||||
// whether we commit or not depends on the appHash
|
||||
for _, block := range chain[:len(chain)-1] {
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
|
||||
state = applyBlock(ctx, t, stateStore, mempool, evpool, state, block, proxyApp, blockStore, eventBus)
|
||||
}
|
||||
|
||||
// apply the final block to a state copy so we can
|
||||
// get the right next appHash but keep the state back
|
||||
applyBlock(ctx, t, stateStore, mempool, evpool, state, chain[len(chain)-1], proxyApp, blockStore)
|
||||
applyBlock(ctx, t, stateStore, mempool, evpool, state, chain[len(chain)-1], proxyApp, blockStore, eventBus)
|
||||
default:
|
||||
require.Fail(t, "unknown mode %v", mode)
|
||||
}
|
||||
@@ -1025,20 +978,23 @@ func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) {
|
||||
|
||||
logger := log.TestingLogger()
|
||||
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
// 2. Tendermint must panic if app returns wrong hash for the first block
|
||||
// - RANDOM HASH
|
||||
// - 0x02
|
||||
// - 0x03
|
||||
{
|
||||
app := &badApp{numBlocks: 3, allHashesAreWrong: true}
|
||||
clientCreator := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(clientCreator, logger, proxy.NopMetrics())
|
||||
client := abciclient.NewLocalClient(logger, app)
|
||||
proxyApp := proxy.New(client, logger, proxy.NopMetrics())
|
||||
err := proxyApp.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { cancel(); proxyApp.Wait() })
|
||||
|
||||
assert.Panics(t, func() {
|
||||
h := NewHandshaker(logger, stateStore, state, store, eventbus.NopEventBus{}, genDoc)
|
||||
h := NewHandshaker(logger, stateStore, state, store, eventBus, genDoc)
|
||||
if err = h.Handshake(ctx, proxyApp); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
@@ -1051,14 +1007,14 @@ func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) {
|
||||
// - RANDOM HASH
|
||||
{
|
||||
app := &badApp{numBlocks: 3, onlyLastHashIsWrong: true}
|
||||
clientCreator := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(clientCreator, logger, proxy.NopMetrics())
|
||||
client := abciclient.NewLocalClient(logger, app)
|
||||
proxyApp := proxy.New(client, logger, proxy.NopMetrics())
|
||||
err := proxyApp.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { cancel(); proxyApp.Wait() })
|
||||
|
||||
assert.Panics(t, func() {
|
||||
h := NewHandshaker(logger, stateStore, state, store, eventbus.NopEventBus{}, genDoc)
|
||||
h := NewHandshaker(logger, stateStore, state, store, eventBus, genDoc)
|
||||
if err = h.Handshake(ctx, proxyApp); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
@@ -1282,12 +1238,16 @@ func TestHandshakeUpdatesValidators(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
votePower := 10 + int64(rand.Uint32())
|
||||
val, _, err := factory.Validator(ctx, votePower)
|
||||
require.NoError(t, err)
|
||||
vals := types.NewValidatorSet([]*types.Validator{val})
|
||||
app := &initChainApp{vals: types.TM2PB.ValidatorUpdates(vals)}
|
||||
clientCreator := abciclient.NewLocalCreator(app)
|
||||
client := abciclient.NewLocalClient(logger, app)
|
||||
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
cfg, err := ResetConfig(t.TempDir(), "handshake_test_")
|
||||
require.NoError(t, err)
|
||||
@@ -1306,9 +1266,8 @@ func TestHandshakeUpdatesValidators(t *testing.T) {
|
||||
genDoc, err := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := log.TestingLogger()
|
||||
handshaker := NewHandshaker(logger, stateStore, state, store, eventbus.NopEventBus{}, genDoc)
|
||||
proxyApp := proxy.NewAppConns(clientCreator, logger, proxy.NopMetrics())
|
||||
handshaker := NewHandshaker(logger, stateStore, state, store, eventBus, genDoc)
|
||||
proxyApp := proxy.New(client, logger, proxy.NopMetrics())
|
||||
require.NoError(t, proxyApp.Start(ctx), "Error starting proxy app connections")
|
||||
|
||||
require.NoError(t, handshaker.Handshake(ctx, proxyApp), "error on abci handshake")
|
||||
|
||||
+82
-45
@@ -20,6 +20,7 @@ import (
|
||||
cstypes "github.com/tendermint/tendermint/internal/consensus/types"
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
"github.com/tendermint/tendermint/internal/jsontypes"
|
||||
"github.com/tendermint/tendermint/internal/libs/autofile"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
tmevents "github.com/tendermint/tendermint/libs/events"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -121,6 +122,9 @@ type State struct {
|
||||
// store blocks and commits
|
||||
blockStore sm.BlockStore
|
||||
|
||||
stateStore sm.Store
|
||||
initialStatePopulated bool
|
||||
|
||||
// create and execute blocks
|
||||
blockExec *sm.BlockExecutor
|
||||
|
||||
@@ -189,18 +193,21 @@ func NewState(
|
||||
ctx context.Context,
|
||||
logger log.Logger,
|
||||
cfg *config.ConsensusConfig,
|
||||
state sm.State,
|
||||
store sm.Store,
|
||||
blockExec *sm.BlockExecutor,
|
||||
blockStore sm.BlockStore,
|
||||
txNotifier txNotifier,
|
||||
evpool evidencePool,
|
||||
eventBus *eventbus.EventBus,
|
||||
options ...StateOption,
|
||||
) *State {
|
||||
) (*State, error) {
|
||||
cs := &State{
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
blockExec: blockExec,
|
||||
blockStore: blockStore,
|
||||
stateStore: store,
|
||||
txNotifier: txNotifier,
|
||||
peerMsgQueue: make(chan msgInfo, msgQueueSize),
|
||||
internalMsgQueue: make(chan msgInfo, msgQueueSize),
|
||||
@@ -220,6 +227,31 @@ func NewState(
|
||||
cs.doPrevote = cs.defaultDoPrevote
|
||||
cs.setProposal = cs.defaultSetProposal
|
||||
|
||||
if err := cs.updateStateFromStore(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE: we do not call scheduleRound0 yet, we do that upon Start()
|
||||
cs.BaseService = *service.NewBaseService(logger, "State", cs)
|
||||
for _, option := range options {
|
||||
option(cs)
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (cs *State) updateStateFromStore(ctx context.Context) error {
|
||||
if cs.initialStatePopulated {
|
||||
return nil
|
||||
}
|
||||
state, err := cs.stateStore.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading state: %w", err)
|
||||
}
|
||||
if state.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We have no votes, so reconstruct LastCommit from SeenCommit.
|
||||
if state.LastBlockHeight > 0 {
|
||||
cs.reconstructLastCommit(state)
|
||||
@@ -227,20 +259,8 @@ func NewState(
|
||||
|
||||
cs.updateToState(ctx, state)
|
||||
|
||||
// NOTE: we do not call scheduleRound0 yet, we do that upon Start()
|
||||
|
||||
cs.BaseService = *service.NewBaseService(logger, "State", cs)
|
||||
for _, option := range options {
|
||||
option(cs)
|
||||
}
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
// SetEventBus sets event bus.
|
||||
func (cs *State) SetEventBus(b *eventbus.EventBus) {
|
||||
cs.eventBus = b
|
||||
cs.blockExec.SetEventBus(b)
|
||||
cs.initialStatePopulated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// StateMetrics sets the metrics.
|
||||
@@ -365,6 +385,10 @@ func (cs *State) LoadCommit(height int64) *types.Commit {
|
||||
// OnStart loads the latest state via the WAL, and starts the timeout and
|
||||
// receive routines.
|
||||
func (cs *State) OnStart(ctx context.Context) error {
|
||||
if err := cs.updateStateFromStore(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We may set the WAL in testing before calling Start, so only OpenWAL if its
|
||||
// still the nilWAL.
|
||||
if _, ok := cs.wal.(nilWAL); ok {
|
||||
@@ -846,15 +870,27 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
cs.logger.Error("CONSENSUS FAILURE!!!", "err", r, "stack", string(debug.Stack()))
|
||||
// stop gracefully
|
||||
//
|
||||
// NOTE: We most probably shouldn't be running any further when there is
|
||||
// some unexpected panic. Some unknown error happened, and so we don't
|
||||
// know if that will result in the validator signing an invalid thing. It
|
||||
// might be worthwhile to explore a mechanism for manual resuming via
|
||||
// some console or secure RPC system, but for now, halting the chain upon
|
||||
// unexpected consensus bugs sounds like the better option.
|
||||
|
||||
// Make a best-effort attempt to close the WAL, but otherwise do not
|
||||
// attempt to gracefully terminate. Once consensus has irrecoverably
|
||||
// failed, any additional progress we permit the node to make may
|
||||
// complicate diagnosing and recovering from the failure.
|
||||
onExit(cs)
|
||||
|
||||
// Re-panic to ensure the node terminates.
|
||||
//
|
||||
// TODO(creachadair): In ordinary operation, the WAL autofile should
|
||||
// never be closed. This only happens during shutdown and production
|
||||
// nodes usually halt by panicking. Many existing tests, however,
|
||||
// assume a clean shutdown is possible. Prior to #8111, we were
|
||||
// swallowing the panic in receiveRoutine, making that appear to
|
||||
// work. Filtering this specific error is slightly risky, but should
|
||||
// affect only unit tests. In any case, not re-panicking here only
|
||||
// preserves the pre-existing behavior for this one error type.
|
||||
if err, ok := r.(error); ok && errors.Is(err, autofile.ErrAutoFileClosed) {
|
||||
return
|
||||
}
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -867,14 +903,11 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) {
|
||||
}
|
||||
}
|
||||
|
||||
rs := cs.RoundState
|
||||
var mi msgInfo
|
||||
|
||||
select {
|
||||
case <-cs.txNotifier.TxsAvailable():
|
||||
cs.handleTxsAvailable(ctx)
|
||||
|
||||
case mi = <-cs.peerMsgQueue:
|
||||
case mi := <-cs.peerMsgQueue:
|
||||
if err := cs.wal.Write(mi); err != nil {
|
||||
cs.logger.Error("failed writing to WAL", "err", err)
|
||||
}
|
||||
@@ -883,11 +916,11 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) {
|
||||
// may generate internal events (votes, complete proposals, 2/3 majorities)
|
||||
cs.handleMsg(ctx, mi)
|
||||
|
||||
case mi = <-cs.internalMsgQueue:
|
||||
case mi := <-cs.internalMsgQueue:
|
||||
err := cs.wal.WriteSync(mi) // NOTE: fsync
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(
|
||||
"failed to write %v msg to consensus WAL due to %v; check your file system and restart the node",
|
||||
panic(fmt.Errorf(
|
||||
"failed to write %v msg to consensus WAL due to %w; check your file system and restart the node",
|
||||
mi, err,
|
||||
))
|
||||
}
|
||||
@@ -902,7 +935,7 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) {
|
||||
|
||||
// if the timeout is relevant to the rs
|
||||
// go to the next step
|
||||
cs.handleTimeout(ctx, ti, rs)
|
||||
cs.handleTimeout(ctx, ti, cs.RoundState)
|
||||
|
||||
case <-ctx.Done():
|
||||
onExit(cs)
|
||||
@@ -964,13 +997,11 @@ func (cs *State) handleMsg(ctx context.Context, mi msgInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// if err == ErrAddingVote {
|
||||
// TODO: punish peer
|
||||
// We probably don't want to stop the peer here. The vote does not
|
||||
// necessarily comes from a malicious peer but can be just broadcasted by
|
||||
// a typical peer.
|
||||
// https://github.com/tendermint/tendermint/issues/1281
|
||||
// }
|
||||
|
||||
// NOTE: the vote is broadcast to peers by the reactor listening
|
||||
// for vote events
|
||||
@@ -1269,8 +1300,16 @@ func (cs *State) defaultDecideProposal(ctx context.Context, height int64, round
|
||||
} else {
|
||||
// Create a new proposal block from state/txs from the mempool.
|
||||
var err error
|
||||
block, blockParts, err = cs.createProposalBlock(ctx)
|
||||
if block == nil || err != nil {
|
||||
block, err = cs.createProposalBlock(ctx)
|
||||
if err != nil {
|
||||
cs.logger.Error("unable to create proposal block", "error", err)
|
||||
return
|
||||
} else if block == nil {
|
||||
return
|
||||
}
|
||||
blockParts, err = block.MakePartSet(types.BlockPartSizeBytes)
|
||||
if err != nil {
|
||||
cs.logger.Error("unable to create proposal block part set", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1329,13 +1368,12 @@ func (cs *State) isProposalComplete() bool {
|
||||
//
|
||||
// NOTE: keep it side-effect free for clarity.
|
||||
// CONTRACT: cs.privValidator is not nil.
|
||||
func (cs *State) createProposalBlock(ctx context.Context) (block *types.Block, blockParts *types.PartSet, err error) {
|
||||
func (cs *State) createProposalBlock(ctx context.Context) (*types.Block, error) {
|
||||
if cs.privValidator == nil {
|
||||
return nil, nil, errors.New("entered createProposalBlock with privValidator being nil")
|
||||
return nil, errors.New("entered createProposalBlock with privValidator being nil")
|
||||
}
|
||||
|
||||
var commit *types.Commit
|
||||
var votes []*types.Vote
|
||||
switch {
|
||||
case cs.Height == cs.state.InitialHeight:
|
||||
// We're creating a proposal for the first block.
|
||||
@@ -1345,23 +1383,22 @@ func (cs *State) createProposalBlock(ctx context.Context) (block *types.Block, b
|
||||
case cs.LastCommit.HasTwoThirdsMajority():
|
||||
// Make the commit from LastCommit
|
||||
commit = cs.LastCommit.MakeCommit()
|
||||
votes = cs.LastCommit.GetVotes()
|
||||
|
||||
default: // This shouldn't happen.
|
||||
cs.logger.Error("propose step; cannot propose anything without commit for the previous block")
|
||||
return
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if cs.privValidatorPubKey == nil {
|
||||
// If this node is a validator & proposer in the current round, it will
|
||||
// miss the opportunity to create a block.
|
||||
cs.logger.Error("propose step; empty priv validator public key", "err", errPubKeyIsNotSet)
|
||||
return
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proposerAddr := cs.privValidatorPubKey.Address()
|
||||
|
||||
return cs.blockExec.CreateProposalBlock(ctx, cs.Height, cs.state, commit, proposerAddr, votes)
|
||||
return cs.blockExec.CreateProposalBlock(ctx, cs.Height, cs.state, commit, proposerAddr, cs.LastCommit.GetVotes())
|
||||
}
|
||||
|
||||
// Enter: `timeoutPropose` after entering Propose.
|
||||
@@ -1880,8 +1917,8 @@ func (cs *State) finalizeCommit(ctx context.Context, height int64) {
|
||||
// restart).
|
||||
endMsg := EndHeightMessage{height}
|
||||
if err := cs.wal.WriteSync(endMsg); err != nil { // NOTE: fsync
|
||||
panic(fmt.Sprintf(
|
||||
"failed to write %v msg to consensus WAL due to %v; check your file system and restart the node",
|
||||
panic(fmt.Errorf(
|
||||
"failed to write %v msg to consensus WAL due to %w; check your file system and restart the node",
|
||||
endMsg, err,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ func TestStateBadProposal(t *testing.T) {
|
||||
proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal)
|
||||
voteCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryVote)
|
||||
|
||||
propBlock, _, err := cs1.createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
propBlock, err := cs1.createProposalBlock(ctx) // changeProposer(t, cs1, vs2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make the second validator the proposer by incrementing round
|
||||
@@ -282,7 +282,7 @@ func TestStateOversizedBlock(t *testing.T) {
|
||||
timeoutProposeCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryTimeoutPropose)
|
||||
voteCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryVote)
|
||||
|
||||
propBlock, _, err := cs1.createProposalBlock(ctx)
|
||||
propBlock, err := cs1.createProposalBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
propBlock.Data.Txs = []types.Tx{tmrand.Bytes(2001)}
|
||||
propBlock.Header.DataHash = propBlock.Data.Hash()
|
||||
@@ -1965,6 +1965,79 @@ func TestProcessProposalAccept(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeBlockCalled(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
voteNil bool
|
||||
expectCalled bool
|
||||
}{
|
||||
{
|
||||
name: "finalze block called when block committed",
|
||||
voteNil: false,
|
||||
expectCalled: true,
|
||||
},
|
||||
{
|
||||
name: "not called when block not committed",
|
||||
voteNil: true,
|
||||
expectCalled: false,
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
config := configSetup(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
m := abcimocks.NewBaseMock()
|
||||
m.On("ProcessProposal", mock.Anything).Return(abcitypes.ResponseProcessProposal{Accept: true})
|
||||
m.On("VerifyVoteExtension", mock.Anything).Return(abcitypes.ResponseVerifyVoteExtension{
|
||||
Result: abcitypes.ResponseVerifyVoteExtension_ACCEPT,
|
||||
})
|
||||
m.On("FinalizeBlock", mock.Anything).Return(abcitypes.ResponseFinalizeBlock{}).Maybe()
|
||||
cs1, vss := makeState(ctx, t, makeStateArgs{config: config, application: m})
|
||||
height, round := cs1.Height, cs1.Round
|
||||
|
||||
proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal)
|
||||
newRoundCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryNewRound)
|
||||
pv1, err := cs1.privValidator.GetPubKey(ctx)
|
||||
require.NoError(t, err)
|
||||
addr := pv1.Address()
|
||||
voteCh := subscribeToVoter(ctx, t, cs1, addr)
|
||||
|
||||
startTestRound(ctx, cs1, cs1.Height, round)
|
||||
ensureNewRound(t, newRoundCh, height, round)
|
||||
ensureNewProposal(t, proposalCh, height, round)
|
||||
rs := cs1.GetRoundState()
|
||||
|
||||
blockID := types.BlockID{}
|
||||
nextRound := round + 1
|
||||
nextHeight := height
|
||||
if !testCase.voteNil {
|
||||
nextRound = 0
|
||||
nextHeight = height + 1
|
||||
blockID = types.BlockID{
|
||||
Hash: rs.ProposalBlock.Hash(),
|
||||
PartSetHeader: rs.ProposalBlockParts.Header(),
|
||||
}
|
||||
}
|
||||
|
||||
signAddVotes(ctx, t, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vss[1:]...)
|
||||
ensurePrevoteMatch(t, voteCh, height, round, rs.ProposalBlock.Hash())
|
||||
|
||||
signAddVotes(ctx, t, cs1, tmproto.PrecommitType, config.ChainID(), blockID, vss[1:]...)
|
||||
ensurePrecommit(t, voteCh, height, round)
|
||||
|
||||
ensureNewRound(t, newRoundCh, nextHeight, nextRound)
|
||||
m.AssertExpectations(t)
|
||||
|
||||
if !testCase.expectCalled {
|
||||
m.AssertNotCalled(t, "FinalizeBlock", mock.Anything)
|
||||
} else {
|
||||
m.AssertCalled(t, "FinalizeBlock", mock.Anything)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 4 vals, 3 Nil Precommits at P0
|
||||
// What we want:
|
||||
// P0 waits for timeoutPrecommit before starting next round
|
||||
@@ -2559,7 +2632,7 @@ func TestStateTimestamp_ProposalNotMatch(t *testing.T) {
|
||||
addr := pv1.Address()
|
||||
voteCh := subscribeToVoter(ctx, t, cs1, addr)
|
||||
|
||||
propBlock, _, err := cs1.createProposalBlock(ctx)
|
||||
propBlock, err := cs1.createProposalBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
round++
|
||||
incrementRound(vss[1:]...)
|
||||
@@ -2607,7 +2680,7 @@ func TestStateTimestamp_ProposalMatch(t *testing.T) {
|
||||
addr := pv1.Address()
|
||||
voteCh := subscribeToVoter(ctx, t, cs1, addr)
|
||||
|
||||
propBlock, _, err := cs1.createProposalBlock(ctx)
|
||||
propBlock, err := cs1.createProposalBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
round++
|
||||
incrementRound(vss[1:]...)
|
||||
|
||||
@@ -30,8 +30,10 @@ import (
|
||||
// stripped down version of node (proxy app, event bus, consensus state) with a
|
||||
// persistent kvstore application and special consensus wal instance
|
||||
// (byteBufferWAL) and waits until numBlocks are created.
|
||||
// If the node fails to produce given numBlocks, it returns an error.
|
||||
func WALGenerateNBlocks(ctx context.Context, t *testing.T, logger log.Logger, wr io.Writer, numBlocks int) (err error) {
|
||||
// If the node fails to produce given numBlocks, it fails the test.
|
||||
func WALGenerateNBlocks(ctx context.Context, t *testing.T, logger log.Logger, wr io.Writer, numBlocks int) {
|
||||
t.Helper()
|
||||
|
||||
cfg := getConfig(t)
|
||||
|
||||
app := kvstore.NewPersistentKVStoreApplication(logger, filepath.Join(cfg.DBDir(), "wal_generator"))
|
||||
@@ -46,41 +48,46 @@ func WALGenerateNBlocks(ctx context.Context, t *testing.T, logger log.Logger, wr
|
||||
privValidatorStateFile := cfg.PrivValidator.StateFile()
|
||||
privValidator, err := privval.LoadOrGenFilePV(privValidatorKeyFile, privValidatorStateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
genDoc, err := types.GenesisDocFromFile(cfg.GenesisFile())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read genesis file: %w", err)
|
||||
t.Fatal(fmt.Errorf("failed to read genesis file: %w", err))
|
||||
}
|
||||
blockStoreDB := dbm.NewMemDB()
|
||||
stateDB := blockStoreDB
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
state, err := sm.MakeGenesisState(genDoc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make genesis state: %w", err)
|
||||
t.Fatal(fmt.Errorf("failed to make genesis state: %w", err))
|
||||
}
|
||||
state.Version.Consensus.App = kvstore.ProtocolVersion
|
||||
if err = stateStore.Save(state); err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blockStore := store.NewBlockStore(blockStoreDB)
|
||||
|
||||
proxyApp := proxy.NewAppConns(abciclient.NewLocalCreator(app), logger.With("module", "proxy"), proxy.NopMetrics())
|
||||
proxyLogger := logger.With("module", "proxy")
|
||||
proxyApp := proxy.New(abciclient.NewLocalClient(logger, app), proxyLogger, proxy.NopMetrics())
|
||||
if err := proxyApp.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start proxy app connections: %w", err)
|
||||
t.Fatal(fmt.Errorf("failed to start proxy app connections: %w", err))
|
||||
}
|
||||
t.Cleanup(proxyApp.Wait)
|
||||
|
||||
eventBus := eventbus.NewDefault(logger.With("module", "events"))
|
||||
if err := eventBus.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start event bus: %w", err)
|
||||
t.Fatal(fmt.Errorf("failed to start event bus: %w", err))
|
||||
}
|
||||
t.Cleanup(func() { eventBus.Stop(); eventBus.Wait() })
|
||||
|
||||
mempool := emptyMempool{}
|
||||
evpool := sm.EmptyEvidencePool{}
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool, blockStore)
|
||||
consensusState := NewState(ctx, logger, cfg.Consensus, state.Copy(), blockExec, blockStore, mempool, evpool)
|
||||
consensusState.SetEventBus(eventBus)
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp, mempool, evpool, blockStore, eventBus)
|
||||
consensusState, err := NewState(ctx, logger, cfg.Consensus, stateStore, blockExec, blockStore, mempool, evpool, eventBus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if privValidator != nil && privValidator != (*privval.FilePV)(nil) {
|
||||
consensusState.SetPrivValidator(ctx, privValidator)
|
||||
}
|
||||
@@ -91,22 +98,24 @@ func WALGenerateNBlocks(ctx context.Context, t *testing.T, logger log.Logger, wr
|
||||
wal := newByteBufferWAL(logger, NewWALEncoder(wr), int64(numBlocks), numBlocksWritten)
|
||||
// see wal.go#103
|
||||
if err := wal.Write(EndHeightMessage{0}); err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
consensusState.wal = wal
|
||||
|
||||
if err := consensusState.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start consensus state: %w", err)
|
||||
t.Fatal(fmt.Errorf("failed to start consensus state: %w", err))
|
||||
}
|
||||
t.Cleanup(consensusState.Wait)
|
||||
|
||||
defer consensusState.Stop()
|
||||
timer := time.NewTimer(time.Minute)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-numBlocksWritten:
|
||||
consensusState.Stop()
|
||||
return nil
|
||||
case <-time.After(1 * time.Minute):
|
||||
consensusState.Stop()
|
||||
return fmt.Errorf("waited too long for tendermint to produce %d blocks (grep logs for `wal_generator`)", numBlocks)
|
||||
case <-timer.C:
|
||||
t.Fatal(fmt.Errorf("waited too long for tendermint to produce %d blocks (grep logs for `wal_generator`)", numBlocks))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +124,7 @@ func WALWithNBlocks(ctx context.Context, t *testing.T, logger log.Logger, numBlo
|
||||
var b bytes.Buffer
|
||||
wr := bufio.NewWriter(&b)
|
||||
|
||||
if err := WALGenerateNBlocks(ctx, t, logger, wr, numBlocks); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
WALGenerateNBlocks(ctx, t, logger, wr, numBlocks)
|
||||
|
||||
wr.Flush()
|
||||
return b.Bytes(), nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package consensus
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"testing"
|
||||
@@ -41,13 +42,12 @@ func TestWALTruncate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = wal.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(wal.Wait)
|
||||
t.Cleanup(func() { wal.Stop(); wal.Group().Stop(); wal.Group().Wait(); wal.Wait() })
|
||||
|
||||
// 60 block's size nearly 70K, greater than group's headBuf size(4096 * 10),
|
||||
// when headBuf is full, truncate content will Flush to the file. at this
|
||||
// time, RotateFile is called, truncate content exist in each file.
|
||||
err = WALGenerateNBlocks(ctx, t, logger, wal.Group(), 60)
|
||||
require.NoError(t, err)
|
||||
WALGenerateNBlocks(ctx, t, logger, wal.Group(), 60)
|
||||
|
||||
// put the leakcheck here so it runs after other cleanup
|
||||
// functions.
|
||||
@@ -112,7 +112,7 @@ func TestWALWrite(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = wal.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(wal.Wait)
|
||||
t.Cleanup(func() { wal.Stop(); wal.Group().Stop(); wal.Group().Wait(); wal.Wait() })
|
||||
|
||||
// 1) Write returns an error if msg is too big
|
||||
msg := &BlockPartMessage{
|
||||
@@ -151,7 +151,6 @@ func TestWALSearchForEndHeight(t *testing.T) {
|
||||
|
||||
wal, err := NewWAL(ctx, logger, walFile)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { wal.Stop(); wal.Wait() })
|
||||
|
||||
h := int64(3)
|
||||
gr, found, err := wal.SearchForEndHeight(h, &WALSearchOptions{})
|
||||
@@ -176,24 +175,24 @@ func TestWALPeriodicSync(t *testing.T) {
|
||||
|
||||
walDir := t.TempDir()
|
||||
walFile := filepath.Join(walDir, "wal")
|
||||
wal, err := NewWAL(ctx, log.TestingLogger(), walFile, autofile.GroupCheckDuration(1*time.Millisecond))
|
||||
defer os.RemoveAll(walFile)
|
||||
|
||||
wal, err := NewWAL(ctx, log.TestingLogger(), walFile, autofile.GroupCheckDuration(250*time.Millisecond))
|
||||
require.NoError(t, err)
|
||||
|
||||
wal.SetFlushInterval(walTestFlushInterval)
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
// Generate some data
|
||||
err = WALGenerateNBlocks(ctx, t, logger, wal.Group(), 5)
|
||||
require.NoError(t, err)
|
||||
WALGenerateNBlocks(ctx, t, logger, wal.Group(), 5)
|
||||
|
||||
// We should have data in the buffer now
|
||||
assert.NotZero(t, wal.Group().Buffered())
|
||||
|
||||
require.NoError(t, wal.Start(ctx))
|
||||
t.Cleanup(func() { wal.Stop(); wal.Wait() })
|
||||
t.Cleanup(func() { wal.Stop(); wal.Group().Stop(); wal.Group().Wait(); wal.Wait() })
|
||||
|
||||
time.Sleep(walTestFlushInterval + (10 * time.Millisecond))
|
||||
time.Sleep(walTestFlushInterval + (20 * time.Millisecond))
|
||||
|
||||
// The data should have been flushed by the periodic sync
|
||||
assert.Zero(t, wal.Group().Buffered())
|
||||
|
||||
@@ -50,13 +50,6 @@ func (b *EventBus) NumClientSubscriptions(clientID string) int {
|
||||
return b.pubsub.NumClientSubscriptions(clientID)
|
||||
}
|
||||
|
||||
// Deprecated: Use SubscribeWithArgs instead.
|
||||
func (b *EventBus) Subscribe(ctx context.Context,
|
||||
clientID string, query *tmquery.Query, capacities ...int) (Subscription, error) {
|
||||
|
||||
return b.pubsub.Subscribe(ctx, clientID, query, capacities...)
|
||||
}
|
||||
|
||||
func (b *EventBus) SubscribeWithArgs(ctx context.Context, args tmpubsub.SubscribeArgs) (Subscription, error) {
|
||||
return b.pubsub.SubscribeWithArgs(ctx, args)
|
||||
}
|
||||
@@ -201,28 +194,3 @@ func (b *EventBus) PublishEventValidatorSetUpdates(ctx context.Context, data typ
|
||||
func (b *EventBus) PublishEventEvidenceValidated(ctx context.Context, evidence types.EventDataEvidenceValidated) error {
|
||||
return b.Publish(ctx, types.EventEvidenceValidatedValue, evidence)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// NopEventBus implements a types.BlockEventPublisher that discards all events.
|
||||
type NopEventBus struct{}
|
||||
|
||||
func (NopEventBus) PublishEventNewBlock(context.Context, types.EventDataNewBlock) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NopEventBus) PublishEventNewBlockHeader(context.Context, types.EventDataNewBlockHeader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NopEventBus) PublishEventNewEvidence(context.Context, types.EventDataNewEvidence) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NopEventBus) PublishEventTx(context.Context, types.EventDataTx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NopEventBus) PublishEventValidatorSetUpdates(context.Context, types.EventDataValidatorSetUpdates) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestEventBusPublishEventTx(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := types.Tx("foo")
|
||||
result := abci.ResponseDeliverTx{
|
||||
result := abci.ExecTxResult{
|
||||
Data: []byte("bar"),
|
||||
Events: []abci.Event{
|
||||
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "baz", Value: "1"}}},
|
||||
@@ -134,7 +134,7 @@ func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := types.Tx("foo")
|
||||
result := abci.ResponseDeliverTx{
|
||||
result := abci.ExecTxResult{
|
||||
Data: []byte("bar"),
|
||||
Events: []abci.Event{
|
||||
{
|
||||
|
||||
+33
-35
@@ -36,14 +36,14 @@ type Pool struct {
|
||||
evidenceList *clist.CList // concurrent linked-list of evidence
|
||||
evidenceSize uint32 // amount of pending evidence
|
||||
|
||||
// needed to load validators to verify evidence
|
||||
stateDB sm.Store
|
||||
// needed to load headers and commits to verify evidence
|
||||
blockStore BlockStore
|
||||
stateDB sm.Store
|
||||
|
||||
mtx sync.Mutex
|
||||
// latest state
|
||||
state sm.State
|
||||
state sm.State
|
||||
isStarted bool
|
||||
// evidence from consensus is buffered to this slice, awaiting until the next height
|
||||
// before being flushed to the pool. This prevents broadcasting and proposing of
|
||||
// evidence before the height with which the evidence happened is finished.
|
||||
@@ -60,46 +60,19 @@ type Pool struct {
|
||||
Metrics *Metrics
|
||||
}
|
||||
|
||||
func (evpool *Pool) SetEventBus(e *eventbus.EventBus) {
|
||||
evpool.eventBus = e
|
||||
}
|
||||
|
||||
// NewPool creates an evidence pool. If using an existing evidence store,
|
||||
// it will add all pending evidence to the concurrent list.
|
||||
func NewPool(logger log.Logger, evidenceDB dbm.DB, stateDB sm.Store, blockStore BlockStore, metrics *Metrics) (*Pool, error) {
|
||||
state, err := stateDB.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load state: %w", err)
|
||||
}
|
||||
|
||||
pool := &Pool{
|
||||
stateDB: stateDB,
|
||||
func NewPool(logger log.Logger, evidenceDB dbm.DB, stateStore sm.Store, blockStore BlockStore, metrics *Metrics, eventBus *eventbus.EventBus) *Pool {
|
||||
return &Pool{
|
||||
blockStore: blockStore,
|
||||
state: state,
|
||||
stateDB: stateStore,
|
||||
logger: logger,
|
||||
evidenceStore: evidenceDB,
|
||||
evidenceList: clist.New(),
|
||||
consensusBuffer: make([]duplicateVoteSet, 0),
|
||||
Metrics: metrics,
|
||||
eventBus: eventBus,
|
||||
}
|
||||
|
||||
// If pending evidence already in db, in event of prior failure, then check
|
||||
// for expiration, update the size and load it back to the evidenceList.
|
||||
pool.pruningHeight, pool.pruningTime = pool.removeExpiredPendingEvidence()
|
||||
evList, _, err := pool.listEvidence(prefixPending, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&pool.evidenceSize, uint32(len(evList)))
|
||||
pool.Metrics.NumEvidence.Set(float64(pool.evidenceSize))
|
||||
|
||||
for _, ev := range evList {
|
||||
pool.evidenceList.PushBack(ev)
|
||||
}
|
||||
pool.eventBus = nil
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// PendingEvidence is used primarily as part of block proposal and returns up to
|
||||
@@ -277,6 +250,31 @@ func (evpool *Pool) State() sm.State {
|
||||
return evpool.state
|
||||
}
|
||||
|
||||
func (evpool *Pool) Start(state sm.State) error {
|
||||
if evpool.isStarted {
|
||||
return errors.New("pool is already running")
|
||||
}
|
||||
|
||||
evpool.state = state
|
||||
|
||||
// If pending evidence already in db, in event of prior failure, then check
|
||||
// for expiration, update the size and load it back to the evidenceList.
|
||||
evpool.pruningHeight, evpool.pruningTime = evpool.removeExpiredPendingEvidence()
|
||||
evList, _, err := evpool.listEvidence(prefixPending, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&evpool.evidenceSize, uint32(len(evList)))
|
||||
evpool.Metrics.NumEvidence.Set(float64(evpool.evidenceSize))
|
||||
|
||||
for _, ev := range evList {
|
||||
evpool.evidenceList.PushBack(ev)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (evpool *Pool) Close() error {
|
||||
return evpool.evidenceStore.Close()
|
||||
}
|
||||
@@ -449,6 +447,7 @@ func (evpool *Pool) listEvidence(prefixKey int64, maxBytes int64) ([]types.Evide
|
||||
}
|
||||
|
||||
func (evpool *Pool) removeExpiredPendingEvidence() (int64, time.Time) {
|
||||
|
||||
batch := evpool.evidenceStore.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
@@ -473,7 +472,6 @@ func (evpool *Pool) removeExpiredPendingEvidence() (int64, time.Time) {
|
||||
|
||||
// remove evidence from the clist
|
||||
evpool.removeEvidenceFromList(blockEvidenceMap)
|
||||
|
||||
// update the evidence size
|
||||
atomic.AddUint32(&evpool.evidenceSize, ^uint32(len(blockEvidenceMap)-1))
|
||||
|
||||
|
||||
@@ -34,6 +34,18 @@ var (
|
||||
defaultEvidenceMaxBytes int64 = 1000
|
||||
)
|
||||
|
||||
func startPool(t *testing.T, pool *evidence.Pool, store sm.Store) {
|
||||
t.Helper()
|
||||
state, err := store.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load state: %v", err)
|
||||
}
|
||||
if err := pool.Start(state); err != nil {
|
||||
t.Fatalf("cannot start state pool: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEvidencePoolBasic(t *testing.T) {
|
||||
var (
|
||||
height = int64(1)
|
||||
@@ -51,9 +63,13 @@ func TestEvidencePoolBasic(t *testing.T) {
|
||||
stateStore.On("LoadValidators", mock.AnythingOfType("int64")).Return(valSet, nil)
|
||||
stateStore.On("Load").Return(createState(height+1, valSet), nil)
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
logger := log.NewNopLogger()
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
pool := evidence.NewPool(logger, evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, pool, stateStore)
|
||||
|
||||
// evidence not seen yet:
|
||||
evs, size := pool.PendingEvidence(defaultEvidenceMaxBytes)
|
||||
require.Equal(t, 0, len(evs))
|
||||
@@ -115,10 +131,12 @@ func TestAddExpiredEvidence(t *testing.T) {
|
||||
return &types.BlockMeta{Header: types.Header{Time: expiredEvidenceTime}}
|
||||
})
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
logger := log.NewNopLogger()
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool := evidence.NewPool(logger, evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, pool, stateStore)
|
||||
|
||||
testCases := []struct {
|
||||
evHeight int64
|
||||
@@ -159,9 +177,7 @@ func TestReportConflictingVotes(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pool, pv := defaultTestPool(ctx, t, height)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool, pv, _ := defaultTestPool(ctx, t, height)
|
||||
|
||||
val := types.NewValidator(pv.PrivKey.PubKey(), 10)
|
||||
|
||||
@@ -201,9 +217,7 @@ func TestEvidencePoolUpdate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pool, val := defaultTestPool(ctx, t, height)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool, val, _ := defaultTestPool(ctx, t, height)
|
||||
|
||||
state := pool.State()
|
||||
|
||||
@@ -273,9 +287,7 @@ func TestVerifyPendingEvidencePasses(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pool, val := defaultTestPool(ctx, t, height)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool, val, _ := defaultTestPool(ctx, t, height)
|
||||
|
||||
ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(
|
||||
ctx,
|
||||
@@ -295,9 +307,7 @@ func TestVerifyDuplicatedEvidenceFails(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pool, val := defaultTestPool(ctx, t, height)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool, val, _ := defaultTestPool(ctx, t, height)
|
||||
|
||||
ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(
|
||||
ctx,
|
||||
@@ -321,7 +331,7 @@ func TestEventOnEvidenceValidated(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pool, val := defaultTestPool(ctx, t, height)
|
||||
pool, val, eventBus := defaultTestPool(ctx, t, height)
|
||||
|
||||
ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(
|
||||
ctx,
|
||||
@@ -332,11 +342,6 @@ func TestEventOnEvidenceValidated(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
eventBus := eventbus.NewDefault(log.TestingLogger())
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
pool.SetEventBus(eventBus)
|
||||
|
||||
const query = `tm.event='EvidenceValidated'`
|
||||
evSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
|
||||
ClientID: "test",
|
||||
@@ -348,6 +353,9 @@ func TestEventOnEvidenceValidated(t *testing.T) {
|
||||
go func() {
|
||||
defer close(done)
|
||||
msg, err := evSub.Next(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
edt := msg.Data().(types.EventDataEvidenceValidated)
|
||||
@@ -394,14 +402,15 @@ func TestLightClientAttackEvidenceLifecycle(t *testing.T) {
|
||||
blockStore.On("LoadBlockCommit", height).Return(trusted.Commit)
|
||||
blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit)
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
logger := log.NewNopLogger()
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool := evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
hash := ev.Hash()
|
||||
|
||||
err = pool.AddEvidence(ctx, ev)
|
||||
err := pool.AddEvidence(ctx, ev)
|
||||
require.NoError(t, err)
|
||||
err = pool.AddEvidence(ctx, ev)
|
||||
require.NoError(t, err)
|
||||
@@ -449,11 +458,13 @@ func TestRecoverPendingEvidence(t *testing.T) {
|
||||
blockStore, err := initializeBlockStore(dbm.NewMemDB(), state, valAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create previous pool and populate it
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
logger := log.NewNopLogger()
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
// create previous pool and populate it
|
||||
pool := evidence.NewPool(logger, evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, pool, stateStore)
|
||||
|
||||
goodEvidence, err := types.NewMockDuplicateVoteEvidenceWithValidator(
|
||||
ctx,
|
||||
@@ -495,9 +506,8 @@ func TestRecoverPendingEvidence(t *testing.T) {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
newPool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, newStateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
|
||||
newPool := evidence.NewPool(logger, evidenceDB, newStateStore, blockStore, evidence.NopMetrics(), nil)
|
||||
startPool(t, newPool, newStateStore)
|
||||
evList, _ := newPool.PendingEvidence(defaultEvidenceMaxBytes)
|
||||
require.Equal(t, 1, len(evList))
|
||||
|
||||
@@ -559,10 +569,7 @@ func initializeBlockStore(db dbm.DB, state sm.State, valAddr []byte) (*store.Blo
|
||||
|
||||
for i := int64(1); i <= state.LastBlockHeight; i++ {
|
||||
lastCommit := makeCommit(i-1, valAddr)
|
||||
block, err := sf.MakeBlock(state, i, lastCommit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block := sf.MakeBlock(state, i, lastCommit)
|
||||
|
||||
block.Header.Time = defaultEvidenceTime.Add(time.Duration(i) * time.Minute)
|
||||
block.Header.Version = version.Consensus{Block: version.BlockProtocol, App: 1}
|
||||
@@ -590,7 +597,7 @@ func makeCommit(height int64, valAddr []byte) *types.Commit {
|
||||
return types.NewCommit(height, 0, types.BlockID{}, commitSigs)
|
||||
}
|
||||
|
||||
func defaultTestPool(ctx context.Context, t *testing.T, height int64) (*evidence.Pool, types.MockPV) {
|
||||
func defaultTestPool(ctx context.Context, t *testing.T, height int64) (*evidence.Pool, types.MockPV, *eventbus.EventBus) {
|
||||
t.Helper()
|
||||
val := types.NewMockPV()
|
||||
valAddress := val.PrivKey.PubKey().Address()
|
||||
@@ -601,10 +608,14 @@ func defaultTestPool(ctx context.Context, t *testing.T, height int64) (*evidence
|
||||
blockStore, err := initializeBlockStore(dbm.NewMemDB(), state, valAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err, "test evidence pool could not be created")
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
return pool, val
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
pool := evidence.NewPool(logger, evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, pool, stateStore)
|
||||
return pool, val, eventBus
|
||||
}
|
||||
|
||||
func createState(height int64, valSet *types.ValidatorSet) sm.State {
|
||||
@@ -616,12 +627,3 @@ func createState(height int64, valSet *types.ValidatorSet) sm.State {
|
||||
ConsensusParams: *types.DefaultConsensusParams(),
|
||||
}
|
||||
}
|
||||
|
||||
func setupEventBus(ctx context.Context, evpool *evidence.Pool) error {
|
||||
eventBus := eventbus.NewDefault(log.TestingLogger())
|
||||
if err := eventBus.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
evpool.SetEventBus(eventBus)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,13 +82,14 @@ func setup(ctx context.Context, t *testing.T, stateStores []sm.Store, chBuf uint
|
||||
}
|
||||
return nil
|
||||
})
|
||||
rts.pools[nodeID], err = evidence.NewPool(logger, evidenceDB, stateStores[idx], blockStore, evidence.NopMetrics())
|
||||
|
||||
require.NoError(t, err)
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
err = eventBus.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
rts.pools[nodeID].SetEventBus(eventBus)
|
||||
|
||||
rts.pools[nodeID] = evidence.NewPool(logger, evidenceDB, stateStores[idx], blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, rts.pools[nodeID], stateStores[idx])
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
rts.peerChans[nodeID] = make(chan p2p.PeerUpdate)
|
||||
rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
"github.com/tendermint/tendermint/internal/evidence"
|
||||
"github.com/tendermint/tendermint/internal/evidence/mocks"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
@@ -76,6 +77,7 @@ func TestVerify_LunaticAttackAgainstState(t *testing.T) {
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
attackTime := defaultEvidenceTime.Add(1 * time.Hour)
|
||||
// create valid lunatic evidence
|
||||
@@ -96,8 +98,7 @@ func TestVerify_LunaticAttackAgainstState(t *testing.T) {
|
||||
blockStore.On("LoadBlockMeta", height).Return(&types.BlockMeta{Header: *trusted.Header})
|
||||
blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit)
|
||||
blockStore.On("LoadBlockCommit", height).Return(trusted.Commit)
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
pool := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), nil)
|
||||
|
||||
evList := types.EvidenceList{ev}
|
||||
// check that the evidence pool correctly verifies the evidence
|
||||
@@ -111,32 +112,29 @@ func TestVerify_LunaticAttackAgainstState(t *testing.T) {
|
||||
// if we submit evidence only against a single byzantine validator when we see there are more validators then this
|
||||
// should return an error
|
||||
ev.ByzantineValidators = ev.ByzantineValidators[:1]
|
||||
t.Log(evList)
|
||||
assert.Error(t, pool.CheckEvidence(ctx, evList))
|
||||
// restore original byz vals
|
||||
ev.ByzantineValidators = ev.GetByzantineValidators(common.ValidatorSet, trusted.SignedHeader)
|
||||
|
||||
// duplicate evidence should be rejected
|
||||
evList = types.EvidenceList{ev, ev}
|
||||
pool, err = evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
pool = evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), nil)
|
||||
assert.Error(t, pool.CheckEvidence(ctx, evList))
|
||||
|
||||
// If evidence is submitted with an altered timestamp it should return an error
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
ev.Timestamp = defaultEvidenceTime.Add(1 * time.Minute)
|
||||
pool, err = evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
pool = evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
|
||||
err = pool.AddEvidence(ctx, ev)
|
||||
err := pool.AddEvidence(ctx, ev)
|
||||
assert.Error(t, err)
|
||||
ev.Timestamp = defaultEvidenceTime
|
||||
|
||||
// Evidence submitted with a different validator power should fail
|
||||
ev.TotalVotingPower = 1
|
||||
pool, err = evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
pool = evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), nil)
|
||||
err = pool.AddEvidence(ctx, ev)
|
||||
assert.Error(t, err)
|
||||
ev.TotalVotingPower = common.ValidatorSet.TotalVotingPower()
|
||||
@@ -154,6 +152,9 @@ func TestVerify_ForwardLunaticAttack(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
// create a forward lunatic attack
|
||||
ev, trusted, common := makeLunaticEvidence(ctx,
|
||||
t, attackHeight, commonHeight, totalVals, byzVals, totalVals-byzVals, defaultEvidenceTime, attackTime)
|
||||
@@ -179,10 +180,11 @@ func TestVerify_ForwardLunaticAttack(t *testing.T) {
|
||||
blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit)
|
||||
blockStore.On("LoadBlockCommit", nodeHeight).Return(trusted.Commit)
|
||||
blockStore.On("Height").Return(nodeHeight)
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
pool := evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
// check that the evidence pool correctly verifies the evidence
|
||||
assert.NoError(t, pool.CheckEvidence(ctx, types.EvidenceList{ev}))
|
||||
@@ -199,8 +201,7 @@ func TestVerify_ForwardLunaticAttack(t *testing.T) {
|
||||
oldBlockStore.On("Height").Return(nodeHeight)
|
||||
require.Equal(t, defaultEvidenceTime, oldBlockStore.LoadBlockMeta(nodeHeight).Header.Time)
|
||||
|
||||
pool, err = evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, oldBlockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
pool = evidence.NewPool(logger, dbm.NewMemDB(), stateStore, oldBlockStore, evidence.NopMetrics(), nil)
|
||||
assert.Error(t, pool.CheckEvidence(ctx, types.EvidenceList{ev}))
|
||||
}
|
||||
|
||||
@@ -208,6 +209,8 @@ func TestVerifyLightClientAttack_Equivocation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
conflictingVals, conflictingPrivVals := factory.ValidatorSet(ctx, t, 5, 10)
|
||||
|
||||
conflictingHeader := factory.MakeHeader(t, &types.Header{
|
||||
@@ -289,10 +292,10 @@ func TestVerifyLightClientAttack_Equivocation(t *testing.T) {
|
||||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader})
|
||||
blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit)
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool := evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
evList := types.EvidenceList{ev}
|
||||
err = pool.CheckEvidence(ctx, evList)
|
||||
@@ -305,6 +308,9 @@ func TestVerifyLightClientAttack_Equivocation(t *testing.T) {
|
||||
func TestVerifyLightClientAttack_Amnesia(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
var height int64 = 10
|
||||
conflictingVals, conflictingPrivVals := factory.ValidatorSet(ctx, t, 5, 10)
|
||||
|
||||
@@ -378,10 +384,10 @@ func TestVerifyLightClientAttack_Amnesia(t *testing.T) {
|
||||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader})
|
||||
blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit)
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool := evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
|
||||
evList := types.EvidenceList{ev}
|
||||
err = pool.CheckEvidence(ctx, evList)
|
||||
@@ -401,6 +407,7 @@ func TestVerifyDuplicateVoteEvidence(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
val := types.NewMockPV()
|
||||
val2 := types.NewMockPV()
|
||||
valSet := types.NewValidatorSet([]*types.Validator{val.ExtractIntoValidator(ctx, 1)})
|
||||
@@ -478,10 +485,11 @@ func TestVerifyDuplicateVoteEvidence(t *testing.T) {
|
||||
blockStore := &mocks.BlockStore{}
|
||||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}})
|
||||
|
||||
pool, err := evidence.NewPool(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics())
|
||||
require.NoError(t, err)
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
require.NoError(t, setupEventBus(ctx, pool))
|
||||
pool := evidence.NewPool(logger, dbm.NewMemDB(), stateStore, blockStore, evidence.NopMetrics(), eventBus)
|
||||
startPool(t, pool, stateStore)
|
||||
|
||||
evList := types.EvidenceList{goodEv}
|
||||
err = pool.CheckEvidence(ctx, evList)
|
||||
|
||||
@@ -265,7 +265,7 @@ func TestBlockResults(t *testing.T) {
|
||||
// tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
stateStoreMock.On("LoadABCIResponses", testHeight).Return(&state.ABCIResponses{
|
||||
FinalizeBlock: &abcitypes.ResponseFinalizeBlock{
|
||||
Txs: []*abcitypes.ResponseDeliverTx{
|
||||
TxResults: []*abcitypes.ExecTxResult{
|
||||
{
|
||||
GasUsed: testGasUsed,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// Create/Append to ./autofile_test
|
||||
af, err := OpenAutoFile("autofile_test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Stream of writes.
|
||||
@@ -32,7 +32,7 @@ for i := 0; i < 60; i++ {
|
||||
// Close the AutoFile
|
||||
err = af.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -41,9 +41,9 @@ const (
|
||||
autoFilePerms = os.FileMode(0600)
|
||||
)
|
||||
|
||||
// errAutoFileClosed is reported when operations attempt to use an autofile
|
||||
// ErrAutoFileClosed is reported when operations attempt to use an autofile
|
||||
// after it has been closed.
|
||||
var errAutoFileClosed = errors.New("autofile is closed")
|
||||
var ErrAutoFileClosed = errors.New("autofile is closed")
|
||||
|
||||
// AutoFile automatically closes and re-opens file for writing. The file is
|
||||
// automatically setup to close itself every 1s and upon receiving SIGHUP.
|
||||
@@ -155,7 +155,7 @@ func (af *AutoFile) Write(b []byte) (n int, err error) {
|
||||
af.mtx.Lock()
|
||||
defer af.mtx.Unlock()
|
||||
if af.closed {
|
||||
return 0, fmt.Errorf("write: %w", errAutoFileClosed)
|
||||
return 0, fmt.Errorf("write: %w", ErrAutoFileClosed)
|
||||
}
|
||||
|
||||
if af.file == nil {
|
||||
@@ -174,7 +174,7 @@ func (af *AutoFile) Write(b []byte) (n int, err error) {
|
||||
func (af *AutoFile) Sync() error {
|
||||
return af.withLock(func() error {
|
||||
if af.closed {
|
||||
return fmt.Errorf("sync: %w", errAutoFileClosed)
|
||||
return fmt.Errorf("sync: %w", ErrAutoFileClosed)
|
||||
} else if af.file == nil {
|
||||
return nil // nothing to sync
|
||||
}
|
||||
@@ -189,13 +189,7 @@ func (af *AutoFile) openFile() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fileInfo, err := file.Stat()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if fileInfo.Mode() != autoFilePerms {
|
||||
// return errors.NewErrPermissionsChanged(file.Name(), fileInfo.Mode(), autoFilePerms)
|
||||
// }
|
||||
|
||||
af.file = file
|
||||
return nil
|
||||
}
|
||||
@@ -207,7 +201,7 @@ func (af *AutoFile) Size() (int64, error) {
|
||||
af.mtx.Lock()
|
||||
defer af.mtx.Unlock()
|
||||
if af.closed {
|
||||
return 0, fmt.Errorf("size: %w", errAutoFileClosed)
|
||||
return 0, fmt.Errorf("size: %w", ErrAutoFileClosed)
|
||||
}
|
||||
|
||||
if af.file == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
@@ -19,19 +20,26 @@ const Version = "0.0.1"
|
||||
const readBufferSize = 1024 // 1KB at a time
|
||||
|
||||
// Parse command-line options
|
||||
func parseFlags() (headPath string, chopSize int64, limitSize int64, version bool) {
|
||||
func parseFlags() (headPath string, chopSize int64, limitSize int64, version bool, err error) {
|
||||
var flagSet = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
var chopSizeStr, limitSizeStr string
|
||||
flagSet.StringVar(&headPath, "head", "logjack.out", "Destination (head) file.")
|
||||
flagSet.StringVar(&chopSizeStr, "chop", "100M", "Move file if greater than this")
|
||||
flagSet.StringVar(&limitSizeStr, "limit", "10G", "Only keep this much (for each specified file). Remove old files.")
|
||||
flagSet.BoolVar(&version, "version", false, "Version")
|
||||
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
||||
fmt.Printf("err parsing flag: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
if err = flagSet.Parse(os.Args[1:]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
chopSize, err = parseByteSize(chopSizeStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
limitSize, err = parseByteSize(limitSizeStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
chopSize = parseBytesize(chopSizeStr)
|
||||
limitSize = parseBytesize(limitSizeStr)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,22 +49,23 @@ func main() {
|
||||
defer func() { fmt.Println("logjack shutting down") }()
|
||||
|
||||
// Read options
|
||||
headPath, chopSize, limitSize, version := parseFlags()
|
||||
headPath, chopSize, limitSize, version, err := parseFlags()
|
||||
if err != nil {
|
||||
stdlog.Fatalf("problem parsing arguments: %q", err.Error())
|
||||
}
|
||||
|
||||
if version {
|
||||
fmt.Printf("logjack version %v\n", Version)
|
||||
return
|
||||
stdlog.Printf("logjack version %s", Version)
|
||||
}
|
||||
|
||||
// Open Group
|
||||
group, err := auto.OpenGroup(ctx, log.NewNopLogger(), headPath, auto.GroupHeadSizeLimit(chopSize), auto.GroupTotalSizeLimit(limitSize))
|
||||
if err != nil {
|
||||
fmt.Printf("logjack couldn't create output file %v\n", headPath)
|
||||
os.Exit(1)
|
||||
stdlog.Fatalf("logjack couldn't create output file %q", headPath)
|
||||
}
|
||||
|
||||
if err = group.Start(ctx); err != nil {
|
||||
fmt.Printf("logjack couldn't start with file %v\n", headPath)
|
||||
os.Exit(1)
|
||||
stdlog.Fatalf("logjack couldn't start with file %q", headPath)
|
||||
}
|
||||
|
||||
// Forever read from stdin and write to AutoFile.
|
||||
@@ -65,25 +74,21 @@ func main() {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
fmt.Println("logjack errored:", err.Error())
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
stdlog.Fatalln("logjack errored:", err.Error())
|
||||
}
|
||||
_, err = group.Write(buf[:n])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "logjack failed write with error %v\n", headPath)
|
||||
os.Exit(1)
|
||||
stdlog.Fatalf("logjack failed write %q with error: %q", headPath, err.Error())
|
||||
}
|
||||
if err := group.FlushAndSync(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "logjack flushsync fail with error %v\n", headPath)
|
||||
os.Exit(1)
|
||||
stdlog.Fatalf("logjack flushsync %q fail with error: %q", headPath, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseBytesize(chopSize string) int64 {
|
||||
func parseByteSize(chopSize string) (int64, error) {
|
||||
// Handle suffix multiplier
|
||||
var multiplier int64 = 1
|
||||
if strings.HasSuffix(chopSize, "T") {
|
||||
@@ -106,8 +111,8 @@ func parseBytesize(chopSize string) int64 {
|
||||
// Parse the numeric part
|
||||
chopSizeInt, err := strconv.Atoi(chopSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int64(chopSizeInt) * multiplier
|
||||
return int64(chopSizeInt) * multiplier, nil
|
||||
}
|
||||
|
||||
@@ -274,6 +274,10 @@ func (g *Group) checkTotalSizeLimit(ctx context.Context) {
|
||||
g.mtx.Lock()
|
||||
defer g.mtx.Unlock()
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if g.totalSizeLimit == 0 {
|
||||
return
|
||||
}
|
||||
@@ -290,6 +294,11 @@ func (g *Group) checkTotalSizeLimit(ctx context.Context) {
|
||||
g.logger.Error("Group's head may grow without bound", "head", g.Head.Path)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pathToRemove := filePathForIndex(g.Head.Path, index, gInfo.MaxIndex)
|
||||
fInfo, err := os.Stat(pathToRemove)
|
||||
if err != nil {
|
||||
@@ -309,11 +318,16 @@ func (g *Group) checkTotalSizeLimit(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// rotateFile causes group to close the current head and assign it some index.
|
||||
// rotateFile causes group to close the current head and assign it
|
||||
// some index. Panics if it encounters an error.
|
||||
func (g *Group) rotateFile(ctx context.Context) {
|
||||
g.mtx.Lock()
|
||||
defer g.mtx.Unlock()
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
headPath := g.Head.Path
|
||||
|
||||
if err := g.headBuf.Flush(); err != nil {
|
||||
|
||||
@@ -275,3 +275,15 @@ func (m *Monitor) waitNextSample(now time.Duration) time.Duration {
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
// CurrentTransferRate returns the current transfer rate
|
||||
func (m *Monitor) CurrentTransferRate() int64 {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.sLast > m.start && m.active {
|
||||
return round(m.rEMA)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestWait(t *testing.T) {
|
||||
defer close(done)
|
||||
got, err := q.Wait(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Wait: unexpected error: %w", err)
|
||||
t.Errorf("Wait: unexpected error: %v", err)
|
||||
} else if got != input {
|
||||
t.Errorf("Wait: got %q, want %q", got, input)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/libs/clist"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -31,7 +31,7 @@ type TxMempool struct {
|
||||
logger log.Logger
|
||||
metrics *Metrics
|
||||
config *config.MempoolConfig
|
||||
proxyAppConn proxy.AppConnMempool
|
||||
proxyAppConn abciclient.Client
|
||||
|
||||
// txsAvailable fires once for each height when the mempool is not empty
|
||||
txsAvailable chan struct{}
|
||||
@@ -93,8 +93,7 @@ type TxMempool struct {
|
||||
func NewTxMempool(
|
||||
logger log.Logger,
|
||||
cfg *config.MempoolConfig,
|
||||
proxyAppConn proxy.AppConnMempool,
|
||||
height int64,
|
||||
proxyAppConn abciclient.Client,
|
||||
options ...TxMempoolOption,
|
||||
) *TxMempool {
|
||||
|
||||
@@ -102,7 +101,7 @@ func NewTxMempool(
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
proxyAppConn: proxyAppConn,
|
||||
height: height,
|
||||
height: -1,
|
||||
cache: NopTxCache{},
|
||||
metrics: NopMetrics(),
|
||||
txStore: NewTxStore(),
|
||||
@@ -418,11 +417,10 @@ func (txmp *TxMempool) Update(
|
||||
ctx context.Context,
|
||||
blockHeight int64,
|
||||
blockTxs types.Txs,
|
||||
deliverTxResponses []*abci.ResponseDeliverTx,
|
||||
execTxResult []*abci.ExecTxResult,
|
||||
newPreFn PreCheckFunc,
|
||||
newPostFn PostCheckFunc,
|
||||
) error {
|
||||
|
||||
txmp.height = blockHeight
|
||||
txmp.notifiedTxsAvailable = false
|
||||
|
||||
@@ -434,7 +432,7 @@ func (txmp *TxMempool) Update(
|
||||
}
|
||||
|
||||
for i, tx := range blockTxs {
|
||||
if deliverTxResponses[i].Code == abci.CodeTypeOK {
|
||||
if execTxResult[i].Code == abci.CodeTypeOK {
|
||||
// add the valid committed transaction to the cache (if missing)
|
||||
_ = txmp.cache.Push(tx)
|
||||
} else if !txmp.config.KeepInvalidTxsInCache {
|
||||
|
||||
@@ -78,24 +78,24 @@ func setup(ctx context.Context, t testing.TB, cacheSize int, options ...TxMempoo
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
|
||||
app := &application{kvstore.NewApplication()}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
logger := log.TestingLogger()
|
||||
|
||||
conn := abciclient.NewLocalClient(logger, &application{
|
||||
kvstore.NewApplication(),
|
||||
})
|
||||
|
||||
cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|"))
|
||||
require.NoError(t, err)
|
||||
cfg.Mempool.CacheSize = cacheSize
|
||||
appConnMem, err := cc(logger)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, appConnMem.Start(ctx))
|
||||
require.NoError(t, conn.Start(ctx))
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(cfg.RootDir)
|
||||
cancel()
|
||||
appConnMem.Wait()
|
||||
conn.Wait()
|
||||
})
|
||||
|
||||
return NewTxMempool(logger.With("test", t.Name()), cfg.Mempool, appConnMem, 0, options...)
|
||||
return NewTxMempool(logger.With("test", t.Name()), cfg.Mempool, conn, options...)
|
||||
}
|
||||
|
||||
func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx {
|
||||
@@ -172,9 +172,9 @@ func TestTxMempool_TxsAvailable(t *testing.T) {
|
||||
rawTxs[i] = tx.tx
|
||||
}
|
||||
|
||||
responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50]))
|
||||
responses := make([]*abci.ExecTxResult, len(rawTxs[:50]))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK}
|
||||
responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
|
||||
}
|
||||
|
||||
// commit half the transactions and ensure we fire an event
|
||||
@@ -204,9 +204,9 @@ func TestTxMempool_Size(t *testing.T) {
|
||||
rawTxs[i] = tx.tx
|
||||
}
|
||||
|
||||
responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50]))
|
||||
responses := make([]*abci.ExecTxResult, len(rawTxs[:50]))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK}
|
||||
responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
|
||||
}
|
||||
|
||||
txmp.Lock()
|
||||
@@ -231,9 +231,9 @@ func TestTxMempool_Flush(t *testing.T) {
|
||||
rawTxs[i] = tx.tx
|
||||
}
|
||||
|
||||
responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50]))
|
||||
responses := make([]*abci.ExecTxResult, len(rawTxs[:50]))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK}
|
||||
responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
|
||||
}
|
||||
|
||||
txmp.Lock()
|
||||
@@ -446,7 +446,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) {
|
||||
for range ticker.C {
|
||||
reapedTxs := txmp.ReapMaxTxs(200)
|
||||
if len(reapedTxs) > 0 {
|
||||
responses := make([]*abci.ResponseDeliverTx, len(reapedTxs))
|
||||
responses := make([]*abci.ExecTxResult, len(reapedTxs))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
var code uint32
|
||||
|
||||
@@ -456,7 +456,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) {
|
||||
code = abci.CodeTypeOK
|
||||
}
|
||||
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: code}
|
||||
responses[i] = &abci.ExecTxResult{Code: code}
|
||||
}
|
||||
|
||||
txmp.Lock()
|
||||
@@ -494,9 +494,9 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) {
|
||||
|
||||
// reap 5 txs at the next height -- no txs should expire
|
||||
reapedTxs := txmp.ReapMaxTxs(5)
|
||||
responses := make([]*abci.ResponseDeliverTx, len(reapedTxs))
|
||||
responses := make([]*abci.ExecTxResult, len(reapedTxs))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK}
|
||||
responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
|
||||
}
|
||||
|
||||
txmp.Lock()
|
||||
@@ -520,9 +520,9 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) {
|
||||
// removed. However, we do know that that at most 95 txs can be expired and
|
||||
// removed.
|
||||
reapedTxs = txmp.ReapMaxTxs(5)
|
||||
responses = make([]*abci.ResponseDeliverTx, len(reapedTxs))
|
||||
responses = make([]*abci.ExecTxResult, len(reapedTxs))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK}
|
||||
responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK}
|
||||
}
|
||||
|
||||
txmp.Lock()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/libs/clist"
|
||||
"github.com/tendermint/tendermint/internal/mempool"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Mempool is an empty implementation of a Mempool, useful for testing.
|
||||
type Mempool struct{}
|
||||
|
||||
var _ Mempool = Mempool{}
|
||||
|
||||
func (Mempool) Lock() {}
|
||||
func (Mempool) Unlock() {}
|
||||
func (Mempool) Size() int { return 0 }
|
||||
func (Mempool) CheckTx(context.Context, types.Tx, func(*abci.ResponseCheckTx), mempool.TxInfo) error {
|
||||
return nil
|
||||
}
|
||||
func (Mempool) RemoveTxByKey(txKey types.TxKey) error { return nil }
|
||||
func (Mempool) ReapMaxBytesMaxGas(_, _ int64) types.Txs { return types.Txs{} }
|
||||
func (Mempool) ReapMaxTxs(n int) types.Txs { return types.Txs{} }
|
||||
func (Mempool) Update(
|
||||
_ context.Context,
|
||||
_ int64,
|
||||
_ types.Txs,
|
||||
_ []*abci.ResponseDeliverTx,
|
||||
_ mempool.PreCheckFunc,
|
||||
_ mempool.PostCheckFunc,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
func (Mempool) Flush() {}
|
||||
func (Mempool) FlushAppConn(ctx context.Context) error { return nil }
|
||||
func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) }
|
||||
func (Mempool) EnableTxsAvailable() {}
|
||||
func (Mempool) SizeBytes() int64 { return 0 }
|
||||
|
||||
func (Mempool) TxsFront() *clist.CElement { return nil }
|
||||
func (Mempool) TxsWaitChan() <-chan struct{} { return nil }
|
||||
|
||||
func (Mempool) InitWAL() error { return nil }
|
||||
func (Mempool) CloseWAL() {}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
abcitypes "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
mempool "github.com/tendermint/tendermint/internal/mempool"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Mempool is an autogenerated mock type for the Mempool type
|
||||
type Mempool struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CheckTx provides a mock function with given fields: ctx, tx, callback, txInfo
|
||||
func (_m *Mempool) CheckTx(ctx context.Context, tx types.Tx, callback func(*abcitypes.ResponseCheckTx), txInfo mempool.TxInfo) error {
|
||||
ret := _m.Called(ctx, tx, callback, txInfo)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, types.Tx, func(*abcitypes.ResponseCheckTx), mempool.TxInfo) error); ok {
|
||||
r0 = rf(ctx, tx, callback, txInfo)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// EnableTxsAvailable provides a mock function with given fields:
|
||||
func (_m *Mempool) EnableTxsAvailable() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// Flush provides a mock function with given fields:
|
||||
func (_m *Mempool) Flush() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// FlushAppConn provides a mock function with given fields: _a0
|
||||
func (_m *Mempool) FlushAppConn(_a0 context.Context) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Lock provides a mock function with given fields:
|
||||
func (_m *Mempool) Lock() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// ReapMaxBytesMaxGas provides a mock function with given fields: maxBytes, maxGas
|
||||
func (_m *Mempool) ReapMaxBytesMaxGas(maxBytes int64, maxGas int64) types.Txs {
|
||||
ret := _m.Called(maxBytes, maxGas)
|
||||
|
||||
var r0 types.Txs
|
||||
if rf, ok := ret.Get(0).(func(int64, int64) types.Txs); ok {
|
||||
r0 = rf(maxBytes, maxGas)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(types.Txs)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ReapMaxTxs provides a mock function with given fields: max
|
||||
func (_m *Mempool) ReapMaxTxs(max int) types.Txs {
|
||||
ret := _m.Called(max)
|
||||
|
||||
var r0 types.Txs
|
||||
if rf, ok := ret.Get(0).(func(int) types.Txs); ok {
|
||||
r0 = rf(max)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(types.Txs)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveTxByKey provides a mock function with given fields: txKey
|
||||
func (_m *Mempool) RemoveTxByKey(txKey types.TxKey) error {
|
||||
ret := _m.Called(txKey)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(types.TxKey) error); ok {
|
||||
r0 = rf(txKey)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Size provides a mock function with given fields:
|
||||
func (_m *Mempool) Size() int {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func() int); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SizeBytes provides a mock function with given fields:
|
||||
func (_m *Mempool) SizeBytes() int64 {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func() int64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// TxsAvailable provides a mock function with given fields:
|
||||
func (_m *Mempool) TxsAvailable() <-chan struct{} {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 <-chan struct{}
|
||||
if rf, ok := ret.Get(0).(func() <-chan struct{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(<-chan struct{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Unlock provides a mock function with given fields:
|
||||
func (_m *Mempool) Unlock() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, blockHeight, blockTxs, txResults, newPreFn, newPostFn
|
||||
func (_m *Mempool) Update(ctx context.Context, blockHeight int64, blockTxs types.Txs, txResults []*abcitypes.ExecTxResult, newPreFn mempool.PreCheckFunc, newPostFn mempool.PostCheckFunc) error {
|
||||
ret := _m.Called(ctx, blockHeight, blockTxs, txResults, newPreFn, newPostFn)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, types.Txs, []*abcitypes.ExecTxResult, mempool.PreCheckFunc, mempool.PostCheckFunc) error); ok {
|
||||
r0 = rf(ctx, blockHeight, blockTxs, txResults, newPreFn, newPostFn)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -242,9 +242,9 @@ func TestReactorConcurrency(t *testing.T) {
|
||||
mempool.Lock()
|
||||
defer mempool.Unlock()
|
||||
|
||||
deliverTxResponses := make([]*abci.ResponseDeliverTx, len(txs))
|
||||
deliverTxResponses := make([]*abci.ExecTxResult, len(txs))
|
||||
for i := range txs {
|
||||
deliverTxResponses[i] = &abci.ResponseDeliverTx{Code: 0}
|
||||
deliverTxResponses[i] = &abci.ExecTxResult{Code: 0}
|
||||
}
|
||||
|
||||
require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, nil, nil))
|
||||
@@ -261,7 +261,7 @@ func TestReactorConcurrency(t *testing.T) {
|
||||
mempool.Lock()
|
||||
defer mempool.Unlock()
|
||||
|
||||
err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ResponseDeliverTx, 0), nil, nil)
|
||||
err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), nil, nil)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ const (
|
||||
MaxActiveIDs = math.MaxUint16
|
||||
)
|
||||
|
||||
//go:generate ../../scripts/mockery_generate.sh Mempool
|
||||
|
||||
// Mempool defines the mempool interface.
|
||||
//
|
||||
// Updates to the mempool need to be synchronized with committing a block so
|
||||
@@ -66,7 +68,7 @@ type Mempool interface {
|
||||
ctx context.Context,
|
||||
blockHeight int64,
|
||||
blockTxs types.Txs,
|
||||
deliverTxResponses []*abci.ResponseDeliverTx,
|
||||
txResults []*abci.ExecTxResult,
|
||||
newPreFn PreCheckFunc,
|
||||
newPostFn PostCheckFunc,
|
||||
) error
|
||||
|
||||
@@ -413,7 +413,7 @@ func (c *MConnection) sendSomePacketMsgs(ctx context.Context) bool {
|
||||
// Block until .sendMonitor says we can write.
|
||||
// Once we're ready we send more than we asked for,
|
||||
// but amortized it should even out.
|
||||
c.sendMonitor.Limit(c._maxPacketMsgSize, atomic.LoadInt64(&c.config.SendRate), true)
|
||||
c.sendMonitor.Limit(c._maxPacketMsgSize, c.config.SendRate, true)
|
||||
|
||||
// Now send some PacketMsgs.
|
||||
for i := 0; i < numBatchPacketMsgs; i++ {
|
||||
@@ -481,7 +481,7 @@ FOR_LOOP:
|
||||
}
|
||||
|
||||
// Block until .recvMonitor says we can read.
|
||||
c.recvMonitor.Limit(c._maxPacketMsgSize, atomic.LoadInt64(&c.config.RecvRate), true)
|
||||
c.recvMonitor.Limit(c._maxPacketMsgSize, c.config.RecvRate, true)
|
||||
|
||||
// Peek into bufConnReader for debugging
|
||||
/*
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestSecretConnectionReadWrite(t *testing.T) {
|
||||
nodePrvKey := ed25519.GenPrivKey()
|
||||
nodeSecretConn, err := MakeSecretConnection(nodeConn, nodePrvKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to establish SecretConnection for node: %w", err)
|
||||
t.Errorf("failed to establish SecretConnection for node: %v", err)
|
||||
return nil, true, err
|
||||
}
|
||||
// In parallel, handle some reads and writes.
|
||||
@@ -136,7 +136,7 @@ func TestSecretConnectionReadWrite(t *testing.T) {
|
||||
for _, nodeWrite := range nodeWrites {
|
||||
n, err := nodeSecretConn.Write([]byte(nodeWrite))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to nodeSecretConn: %w", err)
|
||||
t.Errorf("failed to write to nodeSecretConn: %v", err)
|
||||
return nil, true, err
|
||||
}
|
||||
if n != len(nodeWrite) {
|
||||
@@ -163,7 +163,7 @@ func TestSecretConnectionReadWrite(t *testing.T) {
|
||||
}
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
t.Errorf("failed to read from nodeSecretConn: %w", err)
|
||||
t.Errorf("failed to read from nodeSecretConn: %v", err)
|
||||
return nil, true, err
|
||||
}
|
||||
*nodeReads = append(*nodeReads, string(readBuffer[:n]))
|
||||
@@ -288,7 +288,7 @@ func writeLots(t *testing.T, wg *sync.WaitGroup, conn io.Writer, txt string, n i
|
||||
for i := 0; i < n; i++ {
|
||||
_, err := conn.Write([]byte(txt))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to fooSecConn: %w", err)
|
||||
t.Errorf("failed to write to fooSecConn: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,7 @@ func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection
|
||||
func(_ int) (val interface{}, abort bool, err error) {
|
||||
fooSecConn, err = MakeSecretConnection(fooConn, fooPrvKey)
|
||||
if err != nil {
|
||||
tb.Errorf("failed to establish SecretConnection for foo: %w", err)
|
||||
tb.Errorf("failed to establish SecretConnection for foo: %v", err)
|
||||
return nil, true, err
|
||||
}
|
||||
remotePubBytes := fooSecConn.RemotePubKey()
|
||||
@@ -358,7 +358,7 @@ func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection
|
||||
func(_ int) (val interface{}, abort bool, err error) {
|
||||
barSecConn, err = MakeSecretConnection(barConn, barPrvKey)
|
||||
if barSecConn == nil {
|
||||
tb.Errorf("failed to establish SecretConnection for bar: %w", err)
|
||||
tb.Errorf("failed to establish SecretConnection for bar: %v", err)
|
||||
return nil, true, err
|
||||
}
|
||||
remotePubBytes := barSecConn.RemotePubKey()
|
||||
@@ -405,7 +405,7 @@ func BenchmarkWriteSecretConnection(b *testing.B) {
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
b.Errorf("failed to read from barSecConn: %w", err)
|
||||
b.Errorf("failed to read from barSecConn: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,7 @@ func BenchmarkWriteSecretConnection(b *testing.B) {
|
||||
idx := mrand.Intn(len(fooWriteBytes))
|
||||
_, err := fooSecConn.Write(fooWriteBytes[idx])
|
||||
if err != nil {
|
||||
b.Errorf("failed to write to fooSecConn: %w", err)
|
||||
b.Errorf("failed to write to fooSecConn: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+96
-137
@@ -3,14 +3,12 @@ package pex
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/conn"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
protop2p "github.com/tendermint/tendermint/proto/tendermint/p2p"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -42,7 +40,7 @@ const (
|
||||
minReceiveRequestInterval = 100 * time.Millisecond
|
||||
|
||||
// the maximum amount of addresses that can be included in a response
|
||||
maxAddresses uint16 = 100
|
||||
maxAddresses = 100
|
||||
|
||||
// How long to wait when there are no peers available before trying again
|
||||
noAvailablePeersWaitPeriod = 1 * time.Second
|
||||
@@ -100,15 +98,8 @@ type Reactor struct {
|
||||
// minReceiveRequestInterval).
|
||||
lastReceivedRequests map[types.NodeID]time.Time
|
||||
|
||||
// keep track of how many new peers to existing peers we have received to
|
||||
// extrapolate the size of the network
|
||||
newPeers uint32
|
||||
totalPeers uint32
|
||||
|
||||
// discoveryRatio is the inverse ratio of new peers to old peers squared.
|
||||
// This is multiplied by the minimum duration to calculate how long to wait
|
||||
// between each request.
|
||||
discoveryRatio float32
|
||||
// the total number of unique peers added
|
||||
totalPeers int
|
||||
}
|
||||
|
||||
// NewReactor returns a reference to a new reactor.
|
||||
@@ -156,16 +147,6 @@ func (r *Reactor) OnStop() {}
|
||||
// processPexCh implements a blocking event loop where we listen for p2p
|
||||
// Envelope messages from the pexCh.
|
||||
func (r *Reactor) processPexCh(ctx context.Context) {
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
r.mtx.Lock()
|
||||
var (
|
||||
duration = r.calculateNextRequestTime()
|
||||
err error
|
||||
)
|
||||
r.mtx.Unlock()
|
||||
|
||||
incoming := make(chan *p2p.Envelope)
|
||||
go func() {
|
||||
defer close(incoming)
|
||||
@@ -179,36 +160,51 @@ func (r *Reactor) processPexCh(ctx context.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Initially, we will request peers quickly to bootstrap. This duration
|
||||
// will be adjusted upward as knowledge of the network grows.
|
||||
var nextPeerRequest = minReceiveRequestInterval
|
||||
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
timer.Reset(duration)
|
||||
timer.Reset(nextPeerRequest)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
// outbound requests for new peers
|
||||
case <-timer.C:
|
||||
duration, err = r.sendRequestForPeers(ctx)
|
||||
if err != nil {
|
||||
// Send a request for more peer addresses.
|
||||
if err := r.sendRequestForPeers(ctx); err != nil {
|
||||
return
|
||||
// TODO(creachadair): Do we really want to stop processing the PEX
|
||||
// channel just because of an error here?
|
||||
}
|
||||
// inbound requests for new peers or responses to requests sent by this
|
||||
// reactor
|
||||
|
||||
// Note we do not update the poll timer upon making a request, only
|
||||
// when we receive an update that updates our priors.
|
||||
|
||||
case envelope, ok := <-incoming:
|
||||
if !ok {
|
||||
return
|
||||
return // channel closed
|
||||
}
|
||||
duration, err = r.handleMessage(ctx, r.pexCh.ID, envelope)
|
||||
|
||||
// A request from another peer, or a response to one of our requests.
|
||||
dur, err := r.handlePexMessage(ctx, envelope)
|
||||
if err != nil {
|
||||
r.logger.Error("failed to process message", "ch_id", r.pexCh.ID, "envelope", envelope, "err", err)
|
||||
r.logger.Error("failed to process message",
|
||||
"ch_id", r.pexCh.ID, "envelope", envelope, "err", err)
|
||||
if serr := r.pexCh.SendError(ctx, p2p.PeerError{
|
||||
NodeID: envelope.From,
|
||||
Err: err,
|
||||
}); serr != nil {
|
||||
return
|
||||
}
|
||||
} else if dur != 0 {
|
||||
// We got a useful result; update the poll timer.
|
||||
nextPeerRequest = dur
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,19 +224,20 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) {
|
||||
}
|
||||
|
||||
// handlePexMessage handles envelopes sent from peers on the PexChannel.
|
||||
// If an update was received, a new polling interval is returned; otherwise the
|
||||
// duration is 0.
|
||||
func (r *Reactor) handlePexMessage(ctx context.Context, envelope *p2p.Envelope) (time.Duration, error) {
|
||||
logger := r.logger.With("peer", envelope.From)
|
||||
|
||||
switch msg := envelope.Message.(type) {
|
||||
case *protop2p.PexRequest:
|
||||
// check if the peer hasn't sent a prior request too close to this one
|
||||
// in time
|
||||
// Verify that this peer hasn't sent us another request too recently.
|
||||
if err := r.markPeerRequest(envelope.From); err != nil {
|
||||
return time.Minute, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// request peers from the peer manager and parse the NodeAddresses into
|
||||
// URL strings
|
||||
// Fetch peers from the peer manager, convert NodeAddresses into URL
|
||||
// strings, and send them back to the caller.
|
||||
nodeAddresses := r.peerManager.Advertise(envelope.From, maxAddresses)
|
||||
pexAddresses := make([]protop2p.PexAddress, len(nodeAddresses))
|
||||
for idx, addr := range nodeAddresses {
|
||||
@@ -248,28 +245,24 @@ func (r *Reactor) handlePexMessage(ctx context.Context, envelope *p2p.Envelope)
|
||||
URL: addr.String(),
|
||||
}
|
||||
}
|
||||
if err := r.pexCh.Send(ctx, p2p.Envelope{
|
||||
return 0, r.pexCh.Send(ctx, p2p.Envelope{
|
||||
To: envelope.From,
|
||||
Message: &protop2p.PexResponse{Addresses: pexAddresses},
|
||||
}); err != nil {
|
||||
})
|
||||
|
||||
case *protop2p.PexResponse:
|
||||
// Verify that this response corresponds to one of our pending requests.
|
||||
if err := r.markPeerResponse(envelope.From); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return time.Second, nil
|
||||
case *protop2p.PexResponse:
|
||||
// check if the response matches a request that was made to that peer
|
||||
if err := r.markPeerResponse(envelope.From); err != nil {
|
||||
return time.Minute, err
|
||||
}
|
||||
|
||||
// check the size of the response
|
||||
if len(msg.Addresses) > int(maxAddresses) {
|
||||
return 10 * time.Minute, fmt.Errorf("peer sent too many addresses (max: %d, got: %d)",
|
||||
maxAddresses,
|
||||
len(msg.Addresses),
|
||||
)
|
||||
// Verify that the response does not exceed the safety limit.
|
||||
if len(msg.Addresses) > maxAddresses {
|
||||
return 0, fmt.Errorf("peer sent too many addresses (%d > maxiumum %d)",
|
||||
len(msg.Addresses), maxAddresses)
|
||||
}
|
||||
|
||||
var numAdded int
|
||||
for _, pexAddress := range msg.Addresses {
|
||||
peerAddress, err := p2p.ParseNodeAddress(pexAddress.URL)
|
||||
if err != nil {
|
||||
@@ -278,47 +271,21 @@ func (r *Reactor) handlePexMessage(ctx context.Context, envelope *p2p.Envelope)
|
||||
added, err := r.peerManager.Add(peerAddress)
|
||||
if err != nil {
|
||||
logger.Error("failed to add PEX address", "address", peerAddress, "err", err)
|
||||
continue
|
||||
}
|
||||
if added {
|
||||
r.newPeers++
|
||||
numAdded++
|
||||
logger.Debug("added PEX address", "address", peerAddress)
|
||||
}
|
||||
r.totalPeers++
|
||||
}
|
||||
|
||||
return 10 * time.Minute, nil
|
||||
return r.calculateNextRequestTime(numAdded), nil
|
||||
|
||||
default:
|
||||
return time.Second, fmt.Errorf("received unknown message: %T", msg)
|
||||
return 0, fmt.Errorf("received unknown message: %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage handles an Envelope sent from a peer on a specific p2p Channel.
|
||||
// It will handle errors and any possible panics gracefully. A caller can handle
|
||||
// any error returned by sending a PeerError on the respective channel.
|
||||
func (r *Reactor) handleMessage(ctx context.Context, chID p2p.ChannelID, envelope *p2p.Envelope) (duration time.Duration, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("panic in processing message: %v", e)
|
||||
r.logger.Error(
|
||||
"recovering from processing message panic",
|
||||
"err", err,
|
||||
"stack", string(debug.Stack()),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
r.logger.Debug("received PEX message", "peer", envelope.From)
|
||||
|
||||
switch chID {
|
||||
case p2p.ChannelID(PexChannel):
|
||||
duration, err = r.handlePexMessage(ctx, envelope)
|
||||
default:
|
||||
err = fmt.Errorf("unknown channel ID (%d) for envelope (%v)", chID, envelope)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// processPeerUpdate processes a PeerUpdate. For added peers, PeerStatusUp, we
|
||||
// send a request for addresses.
|
||||
func (r *Reactor) processPeerUpdate(peerUpdate p2p.PeerUpdate) {
|
||||
@@ -338,95 +305,87 @@ func (r *Reactor) processPeerUpdate(peerUpdate p2p.PeerUpdate) {
|
||||
}
|
||||
}
|
||||
|
||||
// sendRequestForPeers pops the first peerID off the list and sends the
|
||||
// peer a request for more peer addresses. The function then moves the
|
||||
// peer into the requestsSent bucket and calculates when the next request
|
||||
// time should be
|
||||
func (r *Reactor) sendRequestForPeers(ctx context.Context) (time.Duration, error) {
|
||||
// sendRequestForPeers chooses a peer from the set of available peers and sends
|
||||
// that peer a request for more peer addresses. The chosen peer is moved into
|
||||
// the requestsSent bucket so that we will not attempt to contact them again
|
||||
// until they've replied or updated.
|
||||
func (r *Reactor) sendRequestForPeers(ctx context.Context) error {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
if len(r.availablePeers) == 0 {
|
||||
// no peers are available
|
||||
r.logger.Debug("no available peers to send request to, waiting...")
|
||||
return noAvailablePeersWaitPeriod, nil
|
||||
r.logger.Debug("no available peers to send a PEX request to (retrying)")
|
||||
return nil
|
||||
}
|
||||
var peerID types.NodeID
|
||||
|
||||
// use range to get a random peer.
|
||||
// Select an arbitrary peer from the available set.
|
||||
var peerID types.NodeID
|
||||
for peerID = range r.availablePeers {
|
||||
break
|
||||
}
|
||||
|
||||
// send out the pex request
|
||||
if err := r.pexCh.Send(ctx, p2p.Envelope{
|
||||
To: peerID,
|
||||
Message: &protop2p.PexRequest{},
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
|
||||
// remove the peer from the abvailable peers list and mark it in the requestsSent map
|
||||
// Move the peer from available to pending.
|
||||
delete(r.availablePeers, peerID)
|
||||
r.requestsSent[peerID] = struct{}{}
|
||||
|
||||
dur := r.calculateNextRequestTime()
|
||||
r.logger.Debug("peer request sent", "next_request_time", dur)
|
||||
return dur, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateNextRequestTime implements something of a proportional controller
|
||||
// to estimate how often the reactor should be requesting new peer addresses.
|
||||
// The dependent variable in this calculation is the ratio of new peers to
|
||||
// all peers that the reactor receives. The interval is thus calculated as the
|
||||
// inverse squared. In the beginning, all peers should be new peers.
|
||||
// We expect this ratio to be near 1 and thus the interval to be as short
|
||||
// as possible. As the node becomes more familiar with the network the ratio of
|
||||
// new nodes will plummet to a very small number, meaning the interval expands
|
||||
// to its upper bound.
|
||||
// calculateNextRequestTime selects how long we should wait before attempting
|
||||
// to send out another request for peer addresses.
|
||||
//
|
||||
// CONTRACT: The caller must hold r.mtx exclusively when calling this method.
|
||||
func (r *Reactor) calculateNextRequestTime() time.Duration {
|
||||
// check if the peer store is full. If so then there is no need
|
||||
// to send peer requests too often
|
||||
// This implements a simplified proportional control mechanism to poll more
|
||||
// often when our knowledge of the network is incomplete, and less often as our
|
||||
// knowledge grows. To estimate our knowledge of the network, we use the
|
||||
// fraction of "new" peers (addresses we have not previously seen) to the total
|
||||
// so far observed. When we first join the network, this fraction will be close
|
||||
// to 1, meaning most new peers are "new" to us, and as we discover more peers,
|
||||
// the fraction will go toward zero.
|
||||
//
|
||||
// The minimum interval will be minReceiveRequestInterval to ensure we will not
|
||||
// request from any peer more often than we would allow them to do from us.
|
||||
func (r *Reactor) calculateNextRequestTime(added int) time.Duration {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.totalPeers += added
|
||||
|
||||
// If the peer store is nearly full, wait the maximum interval.
|
||||
if ratio := r.peerManager.PeerRatio(); ratio >= 0.95 {
|
||||
r.logger.Debug("peer manager near full ratio, sleeping...",
|
||||
r.logger.Debug("Peer manager is nearly full",
|
||||
"sleep_period", fullCapacityInterval, "ratio", ratio)
|
||||
return fullCapacityInterval
|
||||
}
|
||||
|
||||
// baseTime represents the shortest interval that we can send peer requests
|
||||
// in. For example if we have 10 peers and we can't send a message to the
|
||||
// same peer every 500ms, then we can send a request every 50ms. In practice
|
||||
// we use a safety margin of 2, ergo 100ms
|
||||
peers := tmmath.MinInt(len(r.availablePeers), 50)
|
||||
baseTime := minReceiveRequestInterval
|
||||
if peers > 0 {
|
||||
baseTime = minReceiveRequestInterval * 2 / time.Duration(peers)
|
||||
// If there are no available peers to query, poll less aggressively.
|
||||
if len(r.availablePeers) == 0 {
|
||||
r.logger.Debug("No available peers to send a PEX request",
|
||||
"sleep_period", noAvailablePeersWaitPeriod)
|
||||
return noAvailablePeersWaitPeriod
|
||||
}
|
||||
|
||||
if r.totalPeers > 0 || r.discoveryRatio == 0 {
|
||||
// find the ratio of new peers. NOTE: We add 1 to both sides to avoid
|
||||
// divide by zero problems
|
||||
ratio := float32(r.totalPeers+1) / float32(r.newPeers+1)
|
||||
// square the ratio in order to get non linear time intervals
|
||||
// NOTE: The longest possible interval for a network with 100 or more peers
|
||||
// where a node is connected to 50 of them is 2 minutes.
|
||||
r.discoveryRatio = ratio * ratio
|
||||
r.newPeers = 0
|
||||
r.totalPeers = 0
|
||||
}
|
||||
// NOTE: As ratio is always >= 1, discovery ratio is >= 1. Therefore we don't need to worry
|
||||
// about the next request time being less than the minimum time
|
||||
return baseTime * time.Duration(r.discoveryRatio)
|
||||
// Reaching here, there are available peers to query and the peer store
|
||||
// still has space. Estimate our knowledge of the network from the latest
|
||||
// update and choose a new interval.
|
||||
base := float64(minReceiveRequestInterval) / float64(len(r.availablePeers))
|
||||
multiplier := float64(r.totalPeers+1) / float64(added+1) // +1 to avert zero division
|
||||
return time.Duration(base*multiplier*multiplier) + minReceiveRequestInterval
|
||||
}
|
||||
|
||||
func (r *Reactor) markPeerRequest(peer types.NodeID) error {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
if lastRequestTime, ok := r.lastReceivedRequests[peer]; ok {
|
||||
if time.Now().Before(lastRequestTime.Add(minReceiveRequestInterval)) {
|
||||
return fmt.Errorf("peer sent a request too close after a prior one. Minimum interval: %v",
|
||||
minReceiveRequestInterval)
|
||||
if d := time.Since(lastRequestTime); d < minReceiveRequestInterval {
|
||||
return fmt.Errorf("peer %v sent PEX request too soon (%v < minimum %v)",
|
||||
peer, d, minReceiveRequestInterval)
|
||||
}
|
||||
}
|
||||
r.lastReceivedRequests[peer] = time.Now()
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestReactorSendsRequestsTooOften(t *testing.T) {
|
||||
peerErr := <-r.pexErrCh
|
||||
require.Error(t, peerErr.Err)
|
||||
require.Empty(t, r.pexOutCh)
|
||||
require.Contains(t, peerErr.Err.Error(), "peer sent a request too close after a prior one")
|
||||
require.Contains(t, peerErr.Err.Error(), "sent PEX request too soon")
|
||||
require.Equal(t, badNode, peerErr.NodeID)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
"github.com/tendermint/tendermint/abci/types"
|
||||
)
|
||||
|
||||
//go:generate ../../scripts/mockery_generate.sh AppConnConsensus|AppConnMempool|AppConnQuery|AppConnSnapshot
|
||||
|
||||
//----------------------------------------------------------------------------------------
|
||||
// Enforce which abci msgs can be sent on a connection at the type level
|
||||
|
||||
type AppConnConsensus interface {
|
||||
Error() error
|
||||
|
||||
InitChain(context.Context, types.RequestInitChain) (*types.ResponseInitChain, error)
|
||||
|
||||
PrepareProposal(context.Context, types.RequestPrepareProposal) (*types.ResponsePrepareProposal, error)
|
||||
ProcessProposal(context.Context, types.RequestProcessProposal) (*types.ResponseProcessProposal, error)
|
||||
ExtendVote(context.Context, types.RequestExtendVote) (*types.ResponseExtendVote, error)
|
||||
VerifyVoteExtension(context.Context, types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error)
|
||||
FinalizeBlock(context.Context, types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error)
|
||||
Commit(context.Context) (*types.ResponseCommit, error)
|
||||
}
|
||||
|
||||
type AppConnMempool interface {
|
||||
Error() error
|
||||
|
||||
CheckTx(context.Context, types.RequestCheckTx) (*types.ResponseCheckTx, error)
|
||||
|
||||
Flush(context.Context) error
|
||||
}
|
||||
|
||||
type AppConnQuery interface {
|
||||
Error() error
|
||||
|
||||
Echo(context.Context, string) (*types.ResponseEcho, error)
|
||||
Info(context.Context, types.RequestInfo) (*types.ResponseInfo, error)
|
||||
Query(context.Context, types.RequestQuery) (*types.ResponseQuery, error)
|
||||
}
|
||||
|
||||
type AppConnSnapshot interface {
|
||||
Error() error
|
||||
|
||||
ListSnapshots(context.Context, types.RequestListSnapshots) (*types.ResponseListSnapshots, error)
|
||||
OfferSnapshot(context.Context, types.RequestOfferSnapshot) (*types.ResponseOfferSnapshot, error)
|
||||
LoadSnapshotChunk(context.Context, types.RequestLoadSnapshotChunk) (*types.ResponseLoadSnapshotChunk, error)
|
||||
ApplySnapshotChunk(context.Context, types.RequestApplySnapshotChunk) (*types.ResponseApplySnapshotChunk, error)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Implements AppConnConsensus (subset of abciclient.Client)
|
||||
|
||||
type appConnConsensus struct {
|
||||
metrics *Metrics
|
||||
appConn abciclient.Client
|
||||
}
|
||||
|
||||
var _ AppConnConsensus = (*appConnConsensus)(nil)
|
||||
|
||||
func NewAppConnConsensus(appConn abciclient.Client, metrics *Metrics) AppConnConsensus {
|
||||
return &appConnConsensus{
|
||||
metrics: metrics,
|
||||
appConn: appConn,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) Error() error {
|
||||
return app.appConn.Error()
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) InitChain(
|
||||
ctx context.Context,
|
||||
req types.RequestInitChain,
|
||||
) (*types.ResponseInitChain, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "init_chain", "type", "sync"))()
|
||||
return app.appConn.InitChain(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) PrepareProposal(
|
||||
ctx context.Context,
|
||||
req types.RequestPrepareProposal,
|
||||
) (*types.ResponsePrepareProposal, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "prepare_proposal", "type", "sync"))()
|
||||
return app.appConn.PrepareProposal(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) ProcessProposal(
|
||||
ctx context.Context,
|
||||
req types.RequestProcessProposal,
|
||||
) (*types.ResponseProcessProposal, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "process_proposal", "type", "sync"))()
|
||||
return app.appConn.ProcessProposal(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) ExtendVote(
|
||||
ctx context.Context,
|
||||
req types.RequestExtendVote,
|
||||
) (*types.ResponseExtendVote, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "extend_vote", "type", "sync"))()
|
||||
return app.appConn.ExtendVote(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) VerifyVoteExtension(
|
||||
ctx context.Context,
|
||||
req types.RequestVerifyVoteExtension,
|
||||
) (*types.ResponseVerifyVoteExtension, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "verify_vote_extension", "type", "sync"))()
|
||||
return app.appConn.VerifyVoteExtension(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) FinalizeBlock(
|
||||
ctx context.Context,
|
||||
req types.RequestFinalizeBlock,
|
||||
) (*types.ResponseFinalizeBlock, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "finalize_block", "type", "sync"))()
|
||||
return app.appConn.FinalizeBlock(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnConsensus) Commit(ctx context.Context) (*types.ResponseCommit, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "commit", "type", "sync"))()
|
||||
return app.appConn.Commit(ctx)
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
// Implements AppConnMempool (subset of abciclient.Client)
|
||||
|
||||
type appConnMempool struct {
|
||||
metrics *Metrics
|
||||
appConn abciclient.Client
|
||||
}
|
||||
|
||||
func NewAppConnMempool(appConn abciclient.Client, metrics *Metrics) AppConnMempool {
|
||||
return &appConnMempool{
|
||||
metrics: metrics,
|
||||
appConn: appConn,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appConnMempool) Error() error {
|
||||
return app.appConn.Error()
|
||||
}
|
||||
|
||||
func (app *appConnMempool) Flush(ctx context.Context) error {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "flush", "type", "sync"))()
|
||||
return app.appConn.Flush(ctx)
|
||||
}
|
||||
|
||||
func (app *appConnMempool) CheckTx(ctx context.Context, req types.RequestCheckTx) (*types.ResponseCheckTx, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "check_tx", "type", "sync"))()
|
||||
return app.appConn.CheckTx(ctx, req)
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
// Implements AppConnQuery (subset of abciclient.Client)
|
||||
|
||||
type appConnQuery struct {
|
||||
metrics *Metrics
|
||||
appConn abciclient.Client
|
||||
}
|
||||
|
||||
func NewAppConnQuery(appConn abciclient.Client, metrics *Metrics) AppConnQuery {
|
||||
return &appConnQuery{
|
||||
metrics: metrics,
|
||||
appConn: appConn,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appConnQuery) Error() error {
|
||||
return app.appConn.Error()
|
||||
}
|
||||
|
||||
func (app *appConnQuery) Echo(ctx context.Context, msg string) (*types.ResponseEcho, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "echo", "type", "sync"))()
|
||||
return app.appConn.Echo(ctx, msg)
|
||||
}
|
||||
|
||||
func (app *appConnQuery) Info(ctx context.Context, req types.RequestInfo) (*types.ResponseInfo, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "info", "type", "sync"))()
|
||||
return app.appConn.Info(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnQuery) Query(ctx context.Context, reqQuery types.RequestQuery) (*types.ResponseQuery, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "query", "type", "sync"))()
|
||||
return app.appConn.Query(ctx, reqQuery)
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
// Implements AppConnSnapshot (subset of abciclient.Client)
|
||||
|
||||
type appConnSnapshot struct {
|
||||
metrics *Metrics
|
||||
appConn abciclient.Client
|
||||
}
|
||||
|
||||
func NewAppConnSnapshot(appConn abciclient.Client, metrics *Metrics) AppConnSnapshot {
|
||||
return &appConnSnapshot{
|
||||
metrics: metrics,
|
||||
appConn: appConn,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appConnSnapshot) Error() error {
|
||||
return app.appConn.Error()
|
||||
}
|
||||
|
||||
func (app *appConnSnapshot) ListSnapshots(
|
||||
ctx context.Context,
|
||||
req types.RequestListSnapshots,
|
||||
) (*types.ResponseListSnapshots, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "list_snapshots", "type", "sync"))()
|
||||
return app.appConn.ListSnapshots(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnSnapshot) OfferSnapshot(
|
||||
ctx context.Context,
|
||||
req types.RequestOfferSnapshot,
|
||||
) (*types.ResponseOfferSnapshot, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "offer_snapshot", "type", "sync"))()
|
||||
return app.appConn.OfferSnapshot(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnSnapshot) LoadSnapshotChunk(
|
||||
ctx context.Context,
|
||||
req types.RequestLoadSnapshotChunk) (*types.ResponseLoadSnapshotChunk, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "load_snapshot_chunk", "type", "sync"))()
|
||||
return app.appConn.LoadSnapshotChunk(ctx, req)
|
||||
}
|
||||
|
||||
func (app *appConnSnapshot) ApplySnapshotChunk(
|
||||
ctx context.Context,
|
||||
req types.RequestApplySnapshotChunk) (*types.ResponseApplySnapshotChunk, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "apply_snapshot_chunk", "type", "sync"))()
|
||||
return app.appConn.ApplySnapshotChunk(ctx, req)
|
||||
}
|
||||
|
||||
// addTimeSample returns a function that, when called, adds an observation to m.
|
||||
// The observation added to m is the number of seconds ellapsed since addTimeSample
|
||||
// was initially called. addTimeSample is meant to be called in a defer to calculate
|
||||
// the amount of time a function takes to complete.
|
||||
func addTimeSample(m metrics.Histogram) func() {
|
||||
start := time.Now()
|
||||
return func() { m.Observe(time.Since(start).Seconds()) }
|
||||
}
|
||||
+182
-11
@@ -1,42 +1,213 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
"github.com/tendermint/tendermint/abci/example/kvstore"
|
||||
"github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
e2e "github.com/tendermint/tendermint/test/e2e/app"
|
||||
)
|
||||
|
||||
// DefaultClientCreator returns a default ClientCreator, which will create a
|
||||
// local client if addr is one of: 'kvstore',
|
||||
// 'persistent_kvstore', 'e2e', or 'noop', otherwise - a remote client.
|
||||
// ClientFactory returns a client object, which will create a local
|
||||
// client if addr is one of: 'kvstore', 'persistent_kvstore', 'e2e',
|
||||
// or 'noop', otherwise - a remote client.
|
||||
//
|
||||
// The Closer is a noop except for persistent_kvstore applications,
|
||||
// which will clean up the store.
|
||||
func DefaultClientCreator(logger log.Logger, addr, transport, dbDir string) (abciclient.Creator, io.Closer) {
|
||||
func ClientFactory(logger log.Logger, addr, transport, dbDir string) (abciclient.Client, io.Closer, error) {
|
||||
switch addr {
|
||||
case "kvstore":
|
||||
return abciclient.NewLocalCreator(kvstore.NewApplication()), noopCloser{}
|
||||
return abciclient.NewLocalClient(logger, kvstore.NewApplication()), noopCloser{}, nil
|
||||
case "persistent_kvstore":
|
||||
app := kvstore.NewPersistentKVStoreApplication(logger, dbDir)
|
||||
return abciclient.NewLocalCreator(app), app
|
||||
return abciclient.NewLocalClient(logger, app), app, nil
|
||||
case "e2e":
|
||||
app, err := e2e.NewApplication(e2e.DefaultConfig(dbDir))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, noopCloser{}, err
|
||||
}
|
||||
return abciclient.NewLocalCreator(app), noopCloser{}
|
||||
return abciclient.NewLocalClient(logger, app), noopCloser{}, nil
|
||||
case "noop":
|
||||
return abciclient.NewLocalCreator(types.NewBaseApplication()), noopCloser{}
|
||||
return abciclient.NewLocalClient(logger, types.NewBaseApplication()), noopCloser{}, nil
|
||||
default:
|
||||
mustConnect := false // loop retrying
|
||||
return abciclient.NewRemoteCreator(logger, addr, transport, mustConnect), noopCloser{}
|
||||
const mustConnect = false // loop retrying
|
||||
client, err := abciclient.NewClient(logger, addr, transport, mustConnect)
|
||||
if err != nil {
|
||||
return nil, noopCloser{}, err
|
||||
}
|
||||
|
||||
return client, noopCloser{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type noopCloser struct{}
|
||||
|
||||
func (noopCloser) Close() error { return nil }
|
||||
|
||||
// proxyClient provides the application connection.
|
||||
type proxyClient struct {
|
||||
service.BaseService
|
||||
logger log.Logger
|
||||
|
||||
client abciclient.Client
|
||||
metrics *Metrics
|
||||
}
|
||||
|
||||
// New creates a proxy application interface.
|
||||
func New(client abciclient.Client, logger log.Logger, metrics *Metrics) abciclient.Client {
|
||||
conn := &proxyClient{
|
||||
logger: logger,
|
||||
metrics: metrics,
|
||||
client: client,
|
||||
}
|
||||
conn.BaseService = *service.NewBaseService(logger, "proxyClient", conn)
|
||||
return conn
|
||||
}
|
||||
|
||||
func (app *proxyClient) OnStop() { tryCallStop(app.client) }
|
||||
func (app *proxyClient) Error() error { return app.client.Error() }
|
||||
|
||||
func tryCallStop(client abciclient.Client) {
|
||||
if c, ok := client.(interface{ Stop() }); ok {
|
||||
c.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (app *proxyClient) OnStart(ctx context.Context) error {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tryCallStop(app.client)
|
||||
}
|
||||
}()
|
||||
|
||||
// Kill Tendermint if the ABCI application crashes.
|
||||
go func() {
|
||||
if !app.client.IsRunning() {
|
||||
return
|
||||
}
|
||||
app.client.Wait()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.client.Error(); err != nil {
|
||||
app.logger.Error("client connection terminated. Did the application crash? Please restart tendermint",
|
||||
"err", err)
|
||||
|
||||
if killErr := kill(); killErr != nil {
|
||||
app.logger.Error("Failed to kill this process - please do so manually",
|
||||
"err", killErr)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
return app.client.Start(ctx)
|
||||
}
|
||||
|
||||
func kill() error {
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Signal(syscall.SIGABRT)
|
||||
}
|
||||
|
||||
func (app *proxyClient) InitChain(ctx context.Context, req types.RequestInitChain) (*types.ResponseInitChain, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "init_chain", "type", "sync"))()
|
||||
return app.client.InitChain(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) PrepareProposal(ctx context.Context, req types.RequestPrepareProposal) (*types.ResponsePrepareProposal, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "prepare_proposal", "type", "sync"))()
|
||||
return app.client.PrepareProposal(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) ProcessProposal(ctx context.Context, req types.RequestProcessProposal) (*types.ResponseProcessProposal, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "process_proposal", "type", "sync"))()
|
||||
return app.client.ProcessProposal(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) ExtendVote(ctx context.Context, req types.RequestExtendVote) (*types.ResponseExtendVote, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "extend_vote", "type", "sync"))()
|
||||
return app.client.ExtendVote(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) VerifyVoteExtension(ctx context.Context, req types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "verify_vote_extension", "type", "sync"))()
|
||||
return app.client.VerifyVoteExtension(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) FinalizeBlock(ctx context.Context, req types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "finalize_block", "type", "sync"))()
|
||||
return app.client.FinalizeBlock(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) Commit(ctx context.Context) (*types.ResponseCommit, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "commit", "type", "sync"))()
|
||||
return app.client.Commit(ctx)
|
||||
}
|
||||
|
||||
func (app *proxyClient) Flush(ctx context.Context) error {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "flush", "type", "sync"))()
|
||||
return app.client.Flush(ctx)
|
||||
}
|
||||
|
||||
func (app *proxyClient) CheckTx(ctx context.Context, req types.RequestCheckTx) (*types.ResponseCheckTx, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "check_tx", "type", "sync"))()
|
||||
return app.client.CheckTx(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) Echo(ctx context.Context, msg string) (*types.ResponseEcho, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "echo", "type", "sync"))()
|
||||
return app.client.Echo(ctx, msg)
|
||||
}
|
||||
|
||||
func (app *proxyClient) Info(ctx context.Context, req types.RequestInfo) (*types.ResponseInfo, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "info", "type", "sync"))()
|
||||
return app.client.Info(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) Query(ctx context.Context, reqQuery types.RequestQuery) (*types.ResponseQuery, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "query", "type", "sync"))()
|
||||
return app.client.Query(ctx, reqQuery)
|
||||
}
|
||||
|
||||
func (app *proxyClient) ListSnapshots(ctx context.Context, req types.RequestListSnapshots) (*types.ResponseListSnapshots, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "list_snapshots", "type", "sync"))()
|
||||
return app.client.ListSnapshots(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) OfferSnapshot(ctx context.Context, req types.RequestOfferSnapshot) (*types.ResponseOfferSnapshot, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "offer_snapshot", "type", "sync"))()
|
||||
return app.client.OfferSnapshot(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) LoadSnapshotChunk(ctx context.Context, req types.RequestLoadSnapshotChunk) (*types.ResponseLoadSnapshotChunk, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "load_snapshot_chunk", "type", "sync"))()
|
||||
return app.client.LoadSnapshotChunk(ctx, req)
|
||||
}
|
||||
|
||||
func (app *proxyClient) ApplySnapshotChunk(ctx context.Context, req types.RequestApplySnapshotChunk) (*types.ResponseApplySnapshotChunk, error) {
|
||||
defer addTimeSample(app.metrics.MethodTiming.With("method", "apply_snapshot_chunk", "type", "sync"))()
|
||||
return app.client.ApplySnapshotChunk(ctx, req)
|
||||
}
|
||||
|
||||
// addTimeSample returns a function that, when called, adds an observation to m.
|
||||
// The observation added to m is the number of seconds ellapsed since addTimeSample
|
||||
// was initially called. addTimeSample is meant to be called in a defer to calculate
|
||||
// the amount of time a function takes to complete.
|
||||
func addTimeSample(m metrics.Histogram) func() {
|
||||
start := time.Now()
|
||||
return func() { m.Observe(time.Since(start).Seconds()) }
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abcimocks "github.com/tendermint/tendermint/abci/client/mocks"
|
||||
"github.com/tendermint/tendermint/abci/example/kvstore"
|
||||
"github.com/tendermint/tendermint/abci/server"
|
||||
"github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
//----------------------------------------
|
||||
@@ -51,7 +59,10 @@ var SOCKET = "socket"
|
||||
func TestEcho(t *testing.T) {
|
||||
sockPath := fmt.Sprintf("unix:///tmp/echo_%v.sock", tmrand.Str(6))
|
||||
logger := log.TestingLogger()
|
||||
clientCreator := abciclient.NewRemoteCreator(logger, sockPath, SOCKET, true)
|
||||
client, err := abciclient.NewClient(logger, sockPath, SOCKET, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -62,12 +73,9 @@ func TestEcho(t *testing.T) {
|
||||
t.Cleanup(func() { cancel(); s.Wait() })
|
||||
|
||||
// Start client
|
||||
cli, err := clientCreator(logger.With("module", "abci-client"))
|
||||
require.NoError(t, err, "Error creating ABCI client:")
|
||||
require.NoError(t, client.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
require.NoError(t, cli.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
proxy := newAppConnTest(cli)
|
||||
proxy := newAppConnTest(client)
|
||||
t.Log("Connected")
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
@@ -91,7 +99,10 @@ func BenchmarkEcho(b *testing.B) {
|
||||
b.StopTimer() // Initialize
|
||||
sockPath := fmt.Sprintf("unix:///tmp/echo_%v.sock", tmrand.Str(6))
|
||||
logger := log.TestingLogger()
|
||||
clientCreator := abciclient.NewRemoteCreator(logger, sockPath, SOCKET, true)
|
||||
client, err := abciclient.NewClient(logger, sockPath, SOCKET, true)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -102,12 +113,9 @@ func BenchmarkEcho(b *testing.B) {
|
||||
b.Cleanup(func() { cancel(); s.Wait() })
|
||||
|
||||
// Start client
|
||||
cli, err := clientCreator(logger.With("module", "abci-client"))
|
||||
require.NoError(b, err, "Error creating ABCI client")
|
||||
require.NoError(b, client.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
require.NoError(b, cli.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
proxy := newAppConnTest(cli)
|
||||
proxy := newAppConnTest(client)
|
||||
b.Log("Connected")
|
||||
echoString := strings.Repeat(" ", 200)
|
||||
b.StartTimer() // Start benchmarking tests
|
||||
@@ -139,7 +147,10 @@ func TestInfo(t *testing.T) {
|
||||
|
||||
sockPath := fmt.Sprintf("unix:///tmp/echo_%v.sock", tmrand.Str(6))
|
||||
logger := log.TestingLogger()
|
||||
clientCreator := abciclient.NewRemoteCreator(logger, sockPath, SOCKET, true)
|
||||
client, err := abciclient.NewClient(logger, sockPath, SOCKET, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start server
|
||||
s := server.NewSocketServer(logger.With("module", "abci-server"), sockPath, kvstore.NewApplication())
|
||||
@@ -147,12 +158,9 @@ func TestInfo(t *testing.T) {
|
||||
t.Cleanup(func() { cancel(); s.Wait() })
|
||||
|
||||
// Start client
|
||||
cli, err := clientCreator(logger.With("module", "abci-client"))
|
||||
require.NoError(t, err, "Error creating ABCI client")
|
||||
require.NoError(t, client.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
require.NoError(t, cli.Start(ctx), "Error starting ABCI client")
|
||||
|
||||
proxy := newAppConnTest(cli)
|
||||
proxy := newAppConnTest(client)
|
||||
t.Log("Connected")
|
||||
|
||||
resInfo, err := proxy.Info(ctx, RequestInfo)
|
||||
@@ -162,3 +170,65 @@ func TestInfo(t *testing.T) {
|
||||
t.Error("Expected ResponseInfo with one element '{\"size\":0}' but got something else")
|
||||
}
|
||||
}
|
||||
|
||||
type noopStoppableClientImpl struct {
|
||||
abciclient.Client
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *noopStoppableClientImpl) Stop() { c.count++ }
|
||||
|
||||
func TestAppConns_Start_Stop(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
clientMock := &abcimocks.Client{}
|
||||
clientMock.On("Start", mock.Anything).Return(nil)
|
||||
clientMock.On("Error").Return(nil)
|
||||
clientMock.On("IsRunning").Return(true)
|
||||
clientMock.On("Wait").Return(nil).Times(1)
|
||||
cl := &noopStoppableClientImpl{Client: clientMock}
|
||||
|
||||
appConns := New(cl, log.TestingLogger(), NopMetrics())
|
||||
|
||||
err := appConns.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
cancel()
|
||||
appConns.Wait()
|
||||
|
||||
clientMock.AssertExpectations(t)
|
||||
assert.Equal(t, 1, cl.count)
|
||||
}
|
||||
|
||||
// Upon failure, we call tmos.Kill
|
||||
func TestAppConns_Failure(t *testing.T) {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGTERM, syscall.SIGABRT)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientMock := &abcimocks.Client{}
|
||||
clientMock.On("SetLogger", mock.Anything).Return()
|
||||
clientMock.On("Start", mock.Anything).Return(nil)
|
||||
clientMock.On("IsRunning").Return(true)
|
||||
clientMock.On("Wait").Return(nil)
|
||||
clientMock.On("Error").Return(errors.New("EOF"))
|
||||
cl := &noopStoppableClientImpl{Client: clientMock}
|
||||
|
||||
appConns := New(cl, log.TestingLogger(), NopMetrics())
|
||||
|
||||
err := appConns.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { cancel(); appConns.Wait() })
|
||||
|
||||
select {
|
||||
case sig := <-c:
|
||||
t.Logf("signal %q successfully received", sig)
|
||||
case <-ctx.Done():
|
||||
t.Fatal("expected process to receive SIGTERM signal")
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
)
|
||||
|
||||
// AppConns is the Tendermint's interface to the application that consists of
|
||||
// multiple connections.
|
||||
type AppConns interface {
|
||||
service.Service
|
||||
|
||||
// Mempool connection
|
||||
Mempool() AppConnMempool
|
||||
// Consensus connection
|
||||
Consensus() AppConnConsensus
|
||||
// Query connection
|
||||
Query() AppConnQuery
|
||||
// Snapshot connection
|
||||
Snapshot() AppConnSnapshot
|
||||
}
|
||||
|
||||
// NewAppConns calls NewMultiAppConn.
|
||||
func NewAppConns(clientCreator abciclient.Creator, logger log.Logger, metrics *Metrics) AppConns {
|
||||
return NewMultiAppConn(clientCreator, logger, metrics)
|
||||
}
|
||||
|
||||
// multiAppConn implements AppConns.
|
||||
//
|
||||
// A multiAppConn is made of a few appConns and manages their underlying abci
|
||||
// clients.
|
||||
// TODO: on app restart, clients must reboot together
|
||||
type multiAppConn struct {
|
||||
service.BaseService
|
||||
logger log.Logger
|
||||
|
||||
metrics *Metrics
|
||||
consensusConn AppConnConsensus
|
||||
mempoolConn AppConnMempool
|
||||
queryConn AppConnQuery
|
||||
snapshotConn AppConnSnapshot
|
||||
|
||||
client stoppableClient
|
||||
|
||||
clientCreator abciclient.Creator
|
||||
}
|
||||
|
||||
// TODO: this is a totally internal and quasi permanent shim for
|
||||
// clients. eventually we can have a single client and have some kind
|
||||
// of reasonable lifecycle witout needing an explicit stop method.
|
||||
type stoppableClient interface {
|
||||
abciclient.Client
|
||||
Stop()
|
||||
}
|
||||
|
||||
// NewMultiAppConn makes all necessary abci connections to the application.
|
||||
func NewMultiAppConn(clientCreator abciclient.Creator, logger log.Logger, metrics *Metrics) AppConns {
|
||||
multiAppConn := &multiAppConn{
|
||||
logger: logger,
|
||||
metrics: metrics,
|
||||
clientCreator: clientCreator,
|
||||
}
|
||||
multiAppConn.BaseService = *service.NewBaseService(logger, "multiAppConn", multiAppConn)
|
||||
return multiAppConn
|
||||
}
|
||||
|
||||
func (app *multiAppConn) Mempool() AppConnMempool { return app.mempoolConn }
|
||||
func (app *multiAppConn) Consensus() AppConnConsensus { return app.consensusConn }
|
||||
func (app *multiAppConn) Query() AppConnQuery { return app.queryConn }
|
||||
func (app *multiAppConn) Snapshot() AppConnSnapshot { return app.snapshotConn }
|
||||
|
||||
func (app *multiAppConn) OnStart(ctx context.Context) error {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
app.client.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
var client abciclient.Client
|
||||
client, err = app.clientCreator(app.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.queryConn = NewAppConnQuery(client, app.metrics)
|
||||
app.snapshotConn = NewAppConnSnapshot(client, app.metrics)
|
||||
app.mempoolConn = NewAppConnMempool(client, app.metrics)
|
||||
app.consensusConn = NewAppConnConsensus(client, app.metrics)
|
||||
|
||||
app.client = client.(stoppableClient)
|
||||
|
||||
// Kill Tendermint if the ABCI application crashes.
|
||||
go func() {
|
||||
if !client.IsRunning() {
|
||||
return
|
||||
}
|
||||
app.client.Wait()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.client.Error(); err != nil {
|
||||
app.logger.Error("client connection terminated. Did the application crash? Please restart tendermint",
|
||||
"err", err)
|
||||
if killErr := kill(); killErr != nil {
|
||||
app.logger.Error("Failed to kill this process - please do so manually",
|
||||
"err", killErr)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
return client.Start(ctx)
|
||||
}
|
||||
|
||||
func (app *multiAppConn) OnStop() { app.client.Stop() }
|
||||
|
||||
func kill() error {
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Signal(syscall.SIGTERM)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user