mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-12 15:52:50 +00:00
Compare commits
11 Commits
marko/filt
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66ac0bf8b0 | ||
|
|
85a584dd2e | ||
|
|
d0c3457343 | ||
|
|
d433ebe68d | ||
|
|
48147e1fb9 | ||
|
|
b9d6bb4cd1 | ||
|
|
7a84425aec | ||
|
|
488e1d4bd2 | ||
|
|
89246e993a | ||
|
|
6c302218e3 | ||
|
|
023c21f307 |
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@@ -4,7 +4,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
target-branch: "master"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -15,7 +14,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
target-branch: "v0.34.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -26,7 +24,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
target-branch: "v0.35.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -37,7 +34,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
target-branch: "v0.36.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -48,7 +44,6 @@ updates:
|
||||
directory: "/docs"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
###################################
|
||||
@@ -58,7 +53,7 @@ updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
target-branch: "master"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -68,7 +63,7 @@ updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
target-branch: "v0.34.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -78,7 +73,7 @@ updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
target-branch: "v0.35.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
@@ -88,7 +83,7 @@ updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
target-branch: "v0.36.x"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish to Docker Hub
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./DOCKER/Dockerfile
|
||||
|
||||
8
.github/workflows/docs-deployment.yml
vendored
8
.github/workflows/docs-deployment.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Install generator dependencies
|
||||
run: |
|
||||
apk add --no-cache make bash git npm
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# We need to fetch full history so the backport branches for previous
|
||||
# versions will be available for the build.
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
git config --global --add safe.directory "$PWD"
|
||||
make build-docs
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output
|
||||
path: ~/output/
|
||||
@@ -49,8 +49,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-output
|
||||
path: ~/output
|
||||
|
||||
2
Makefile
2
Makefile
@@ -224,7 +224,7 @@ build-docs:
|
||||
@cd docs && \
|
||||
while read -r branch path_prefix; do \
|
||||
( git checkout $${branch} && npm ci --quiet && \
|
||||
VUEPRESS_BASE="/$${path_prefix}/" npm run build --quiet ) ; \
|
||||
VUEPRESS_BASE="/$${path_prefix}/" NODE_OPTIONS="--openssl-legacy-provider" npm run build --quiet ) ; \
|
||||
mkdir -p ~/output/$${path_prefix} ; \
|
||||
cp -r .vuepress/dist/* ~/output/$${path_prefix}/ ; \
|
||||
cp ~/output/$${path_prefix}/index.html ~/output ; \
|
||||
|
||||
21
README.md
21
README.md
@@ -36,20 +36,17 @@ Please do not depend on master as your production branch. Use [releases](https:/
|
||||
Tendermint has been in the production of private and public environments, most notably the blockchains of the Cosmos Network. we haven't released v1.0 yet since we are making breaking changes to the protocol and the APIs.
|
||||
See below for more details about [versioning](#versioning).
|
||||
|
||||
In any case, if you intend to run Tendermint in production, we're happy to help. You can
|
||||
contact us [over email](mailto:hello@interchain.io) or [join the chat](https://discord.gg/cosmosnetwork).
|
||||
In any case, if you intend to run Tendermint in production, we're happy to help.
|
||||
You can contact us [over email](mailto:hello@newtendermint.org) or [join the
|
||||
chat](https://discord.gg/gnoland).
|
||||
|
||||
More on how releases are conducted can be found [here](./RELEASES.md).
|
||||
|
||||
## Security
|
||||
|
||||
To report a security vulnerability, see our [bug bounty
|
||||
program](https://hackerone.com/cosmos).
|
||||
To report a security vulnerability, please [email us](mailto:security@newtendermint.org).
|
||||
For examples of the kinds of bugs we're looking for, see [our security policy](SECURITY.md).
|
||||
|
||||
We also maintain a dedicated mailing list for security updates. We will only ever use this mailing list
|
||||
to notify you of vulnerabilities and fixes in Tendermint Core. You can subscribe [here](http://eepurl.com/gZ5hQD).
|
||||
|
||||
## Minimum requirements
|
||||
|
||||
| Requirement | Notes |
|
||||
@@ -139,9 +136,9 @@ We keep a public up-to-date version of our roadmap [here](./docs/roadmap/roadmap
|
||||
|
||||
## Join us!
|
||||
|
||||
Tendermint Core is maintained by [Interchain GmbH](https://interchain.berlin).
|
||||
If you'd like to work full-time on Tendermint Core, [we're hiring](https://interchain-gmbh.breezy.hr/)!
|
||||
The development of Tendermint Core was led primarily by All in Bits, Inc. The
|
||||
Tendermint trademark is owned by New Tendermint, LLC. If you'd like to work
|
||||
full-time on Tendermint2 or [gno.land](https://gno.land), [we're
|
||||
hiring](mailto:hiring@newtendermint.org)!
|
||||
|
||||
|
||||
Funding for Tendermint Core development comes primarily from the [Interchain Foundation](https://interchain.io),
|
||||
a Swiss non-profit. The Tendermint trademark is owned by [Tendermint Inc.](https://tendermint.com), the for-profit entity
|
||||
that also maintains [tendermint.com](https://tendermint.com).
|
||||
|
||||
168
RELEASES.md
168
RELEASES.md
@@ -1,8 +1,9 @@
|
||||
# Releases
|
||||
|
||||
Tendermint uses [semantic versioning](https://semver.org/) with each release following
|
||||
a `vX.Y.Z` format. The `master` branch is used for active development and thus it's
|
||||
advisable not to build against it.
|
||||
Tendermint uses modified [semantic versioning](https://semver.org/) with each
|
||||
release following a `vX.Y.Z` format. Tendermint is currently on major version
|
||||
0 and uses the minor version to signal breaking changes. The `master` branch is
|
||||
used for active development and thus it is not advisable to build against it.
|
||||
|
||||
The latest changes are always initially merged into `master`.
|
||||
Releases are specified using tags and are built from long-lived "backport" branches
|
||||
@@ -29,8 +30,8 @@ merging the pull request.
|
||||
|
||||
### Creating a backport branch
|
||||
|
||||
If this is the first release candidate for a major release, you get to have the
|
||||
honor of creating the backport branch!
|
||||
If this is the first release candidate for a minor version release, e.g.
|
||||
v0.25.0, you get to have the honor of creating the backport branch!
|
||||
|
||||
Note that, after creating the backport branch, you'll also need to update the
|
||||
tags on `master` so that `go mod` is able to order the branches correctly. You
|
||||
@@ -77,7 +78,8 @@ the 0.35.x line.
|
||||
|
||||
After doing these steps, go back to `master` and do the following:
|
||||
|
||||
1. Tag `master` as the dev branch for the _next_ major release and push it up to GitHub.
|
||||
1. Tag `master` as the dev branch for the _next_ minor version release and push
|
||||
it up to GitHub.
|
||||
For example:
|
||||
```sh
|
||||
git tag -a v0.36.0-dev -m "Development base for Tendermint v0.36."
|
||||
@@ -99,7 +101,7 @@ After doing these steps, go back to `master` and do the following:
|
||||
|
||||
## Release candidates
|
||||
|
||||
Before creating an official release, especially a major release, we may want to create a
|
||||
Before creating an official release, especially a minor release, we may want to create a
|
||||
release candidate (RC) for our friends and partners to test out. We use git tags to
|
||||
create RCs, and we build them off of backport branches.
|
||||
|
||||
@@ -109,7 +111,7 @@ Tags for RCs should follow the "standard" release naming conventions, with `-rcX
|
||||
(Note that branches and tags _cannot_ have the same names, so it's important that these branches
|
||||
have distinct names from the tags/release names.)
|
||||
|
||||
If this is the first RC for a major release, you'll have to make a new backport branch (see above).
|
||||
If this is the first RC for a minor release, you'll have to make a new backport branch (see above).
|
||||
Otherwise:
|
||||
|
||||
1. Start from the backport branch (e.g. `v0.35.x`).
|
||||
@@ -140,11 +142,13 @@ Note that this process should only be used for "true" RCs--
|
||||
release candidates that, if successful, will be the next release.
|
||||
For more experimental "RCs," create a new, short-lived branch and tag that instead.
|
||||
|
||||
## Major release
|
||||
## Minor release
|
||||
|
||||
This major release process assumes that this release was preceded by release candidates.
|
||||
This minor release process assumes that this release was preceded by release candidates.
|
||||
If there were no release candidates, begin by creating a backport branch, as described above.
|
||||
|
||||
Before performing these steps, be sure the [Minor Release Checklist](#minor-release-checklist) has been completed.
|
||||
|
||||
1. Start on the backport branch (e.g. `v0.35.x`)
|
||||
2. Run integration tests (`make test_integrations`) and the e2e nightlies.
|
||||
3. Prepare the release:
|
||||
@@ -176,16 +180,16 @@ If there were no release candidates, begin by creating a backport branch, as des
|
||||
- Commit these changes to `master` and backport them into the backport
|
||||
branch for this release.
|
||||
|
||||
## Minor release (point releases)
|
||||
## Patch release
|
||||
|
||||
Minor releases are done differently from major releases: They are built off of
|
||||
Patch releases are done differently from minor releases: They are built off of
|
||||
long-lived backport branches, rather than from master. As non-breaking changes
|
||||
land on `master`, they should also be backported into these backport branches.
|
||||
|
||||
Minor releases don't have release candidates by default, although any tricky
|
||||
Patch releases don't have release candidates by default, although any tricky
|
||||
changes may merit a release candidate.
|
||||
|
||||
To create a minor release:
|
||||
To create a patch release:
|
||||
|
||||
1. Checkout the long-lived backport branch: `git checkout v0.35.x`
|
||||
2. Run integration tests (`make test_integrations`) and the nightlies.
|
||||
@@ -197,11 +201,143 @@ To create a minor release:
|
||||
- Bump the TMDefaultVersion in `version.go`
|
||||
- Bump the ABCI version number, if necessary.
|
||||
(Note that ABCI follows semver, and that ABCI versions are the only versions
|
||||
which can change during minor releases, and only field additions are valid minor changes.)
|
||||
which can change during patch releases, and only field additions are valid patch changes.)
|
||||
4. Open a PR with these changes that will land them back on `v0.35.x`
|
||||
5. Once this change has landed on the backport branch, make sure to pull it locally, then push a tag.
|
||||
- `git tag -a v0.35.1 -m 'Release v0.35.1'`
|
||||
- `git push origin v0.35.1`
|
||||
6. Create a pull request back to master with the CHANGELOG & version changes from the latest release.
|
||||
- Remove all `R:minor` labels from the pull requests that were included in the release.
|
||||
- Remove all `R:patch` labels from the pull requests that were included in the release.
|
||||
- Do not merge the backport branch into master.
|
||||
|
||||
## Minor Release Checklist
|
||||
|
||||
The following set of steps are performed on all releases that increment the
|
||||
_minor_ version, e.g. v0.25 to v0.26. These steps ensure that Tendermint is
|
||||
well tested, stable, and suitable for adoption by the various diverse projects
|
||||
that rely on Tendermint.
|
||||
|
||||
### Feature Freeze
|
||||
|
||||
Ahead of any minor version release of Tendermint, the software enters 'Feature
|
||||
Freeze' for at least two weeks. A feature freeze means that _no_ new features
|
||||
are added to the code being prepared for release. No code changes should be made
|
||||
to the code being released that do not directly improve pressing issues of code
|
||||
quality. The following must not be merged during a feature freeze:
|
||||
|
||||
* Refactors that are not related to specific bug fixes.
|
||||
* Dependency upgrades.
|
||||
* New test code that does not test a discovered regression.
|
||||
* New features of any kind.
|
||||
* Documentation or spec improvements that are not related to the newly developed
|
||||
code.
|
||||
|
||||
This period directly follows the creation of the [backport
|
||||
branch](#creating-a-backport-branch). The Tendermint team instead directs all
|
||||
attention to ensuring that the existing code is stable and reliable. Broken
|
||||
tests are fixed, flakey-tests are remedied, end-to-end test failures are
|
||||
thoroughly diagnosed and all efforts of the team are aimed at improving the
|
||||
quality of the code. During this period, the upgrade harness tests are run
|
||||
repeatedly and a variety of in-house testnets are run to ensure Tendermint
|
||||
functions at the scale it will be used by application developers and node
|
||||
operators.
|
||||
|
||||
### Nightly End-To-End Tests
|
||||
|
||||
The Tendermint team maintains [a set of end-to-end
|
||||
tests](https://github.com/tendermint/tendermint/blob/master/test/e2e/README.md#L1)
|
||||
that run each night on the latest commit of the project and on the code in the
|
||||
tip of each supported backport branch. These tests start a network of containerized
|
||||
Tendermint processes and run automated checks that the network functions as
|
||||
expected in both stable and unstable conditions. During the feature freeze,
|
||||
these tests are run nightly and must pass consistently for a release of
|
||||
Tendermint to be considered stable.
|
||||
|
||||
### Upgrade Harness
|
||||
|
||||
> TODO(williambanfield): Change to past tense and clarify this section once
|
||||
> upgrade harness is complete.
|
||||
|
||||
The Tendermint team is creating an upgrade test harness to exercise the
|
||||
workflow of stopping an instance of Tendermint running one version of the
|
||||
software and starting up the same application running the next version. To
|
||||
support upgrade testing, we will add the ability to terminate the Tendermint
|
||||
process at specific pre-defined points in its execution so that we can verify
|
||||
upgrades work in a representative sample of stop conditions.
|
||||
|
||||
### Large Scale Testnets
|
||||
|
||||
The Tendermint end-to-end tests run a small network (~10s of nodes) to exercise
|
||||
basic consensus interactions. Real world deployments of Tendermint often have over
|
||||
a hundred nodes just in the validator set, with many others acting as full
|
||||
nodes and sentry nodes. To gain more assurance before a release, we will also run
|
||||
larger-scale test networks to shake out emergent behaviors at scale.
|
||||
|
||||
Large-scale test networks are run on a set of virtual machines (VMs). Each VM
|
||||
is equipped with 4 Gigabytes of RAM and 2 CPU cores. The network runs a very
|
||||
simple key-value store application. The application adds artificial delays to
|
||||
different ABCI calls to simulate a slow application. Each testnet is briefly
|
||||
run with no load being generated to collect a baseline performance. Once
|
||||
baseline is captured, a consistent load is applied across the network. This
|
||||
load takes the form of 10% of the running nodes all receiving a consistent
|
||||
stream of two hundred transactions per minute each.
|
||||
|
||||
During each test net, the following metrics are monitored and collected on each
|
||||
node:
|
||||
|
||||
* Consensus rounds per height
|
||||
* Maximum connected peers, Minimum connected peers, Rate of change of peer connections
|
||||
* Memory resident set size
|
||||
* CPU utilization
|
||||
* Blocks produced per minute
|
||||
* Seconds for each step of consensus (Propose, Prevote, Precommit, Commit)
|
||||
* Latency to receive block proposals
|
||||
|
||||
For these tests we intentionally target low-powered host machines (with low core
|
||||
counts and limited memory) to ensure we observe similar kinds of resource contention
|
||||
and limitation that real-world deployments of Tendermint experience in production.
|
||||
|
||||
#### 200 Node Testnet
|
||||
|
||||
To test the stability and performance of Tendermint in a real world scenario,
|
||||
a 200 node test network is run. The network comprises 5 seed nodes, 100
|
||||
validators and 95 non-validating full nodes. All nodes begin by dialing
|
||||
a subset of the seed nodes to discover peers. The network is run for several
|
||||
days, with metrics being collected continuously. In cases of changes to performance
|
||||
critical systems, testnets of larger sizes should be considered.
|
||||
|
||||
#### Rotating Node Testnet
|
||||
|
||||
Real-world deployments of Tendermint frequently see new nodes arrive and old
|
||||
nodes exit the network. The rotating node testnet ensures that Tendermint is
|
||||
able to handle this reliably. In this test, a network with 10 validators and
|
||||
3 seed nodes is started. A rolling set of 25 full nodes are started and each
|
||||
connects to the network by dialing one of the seed nodes. Once the node is able
|
||||
to blocksync to the head of the chain and begins producing blocks using
|
||||
Tendermint consensus it is stopped. Once stopped, a new node is started and
|
||||
takes its place. This network is run for several days.
|
||||
|
||||
#### Network Partition Testnet
|
||||
|
||||
Tendermint is expected to recover from network partitions. A partition where no
|
||||
subset of the nodes is left with the super-majority of the stake is expected to
|
||||
stop making blocks. Upon alleviation of the partition, the network is expected
|
||||
to once again become fully connected and capable of producing blocks. The
|
||||
network partition testnet ensures that Tendermint is able to handle this
|
||||
reliably at scale. In this test, a network with 100 validators and 95 full
|
||||
nodes is started. All validators have equal stake. Once the network is
|
||||
producing blocks, a set of firewall rules is deployed to create a partitioned
|
||||
network with 50% of the stake on one side and 50% on the other. Once the
|
||||
network stops producing blocks, the firewall rules are removed and the nodes
|
||||
are monitored to ensure they reconnect and that the network again begins
|
||||
producing blocks.
|
||||
|
||||
#### Absent Stake Testnet
|
||||
|
||||
Tendermint networks often run with _some_ portion of the voting power offline.
|
||||
The absent stake testnet ensures that large networks are able to handle this
|
||||
reliably. A set of 150 validator nodes and three seed nodes is started. The set
|
||||
of 150 validators is configured to only possess a cumulative stake of 67% of
|
||||
the total stake. The remaining 33% of the stake is configured to belong to
|
||||
a validator that is never actually run in the test network. The network is run
|
||||
for multiple days, ensuring that it is able to produce blocks without issue.
|
||||
|
||||
@@ -3,7 +3,6 @@ package abciclient
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -65,7 +64,7 @@ RETRY_LOOP:
|
||||
if cli.mustConnect {
|
||||
return err
|
||||
}
|
||||
cli.logger.Error(fmt.Sprintf("abci.grpcClient failed to connect to %v. Retrying...\n", cli.addr), "err", err)
|
||||
cli.logger.Error("abci.grpcClient failed to connect, Retrying...", "addr", cli.addr, "err", err)
|
||||
timer.Reset(time.Second * dialRetryIntervalSeconds)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -67,8 +67,11 @@ func (cli *socketClient) OnStart(ctx context.Context) error {
|
||||
if cli.mustConnect {
|
||||
return err
|
||||
}
|
||||
cli.logger.Error(fmt.Sprintf("abci.socketClient failed to connect to %v. Retrying after %vs...",
|
||||
cli.addr, dialRetryIntervalSeconds), "err", err)
|
||||
|
||||
cli.logger.Error("abci.socketClient failed to connect, retrying after",
|
||||
"retry_after", dialRetryIntervalSeconds,
|
||||
"target", cli.addr,
|
||||
"err", err)
|
||||
|
||||
timer.Reset(time.Second * dialRetryIntervalSeconds)
|
||||
select {
|
||||
@@ -77,7 +80,6 @@ func (cli *socketClient) OnStart(ctx context.Context) error {
|
||||
case <-timer.C:
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
cli.conn = conn
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
)
|
||||
|
||||
func TestRollbackIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
var height int64
|
||||
dir := t.TempDir()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -106,7 +106,7 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
smallprint:
|
||||
'The development of Tendermint Core is led primarily by [Interchain GmbH](https://interchain.berlin/). Funding for this development comes primarily from the Interchain Foundation, a Swiss non-profit. The Tendermint trademark is owned by Tendermint Inc, the for-profit entity that also maintains this website.',
|
||||
'The development of Tendermint Core was led primarily by All in Bits, Inc. The Tendermint trademark is owned by New Tendermint, LLC.',
|
||||
links: [
|
||||
{
|
||||
title: 'Documentation',
|
||||
|
||||
@@ -31,3 +31,4 @@ To find out about the Tendermint ecosystem you can go [here](https://github.com/
|
||||
## Contribute
|
||||
|
||||
To contribute to the documentation, see [this file](https://github.com/tendermint/tendermint/blob/master/docs/DOCS_README.md) for details of the build process and considerations when making changes.
|
||||
|
||||
|
||||
@@ -247,8 +247,8 @@ Similarly, you could put the commands in a file and run
|
||||
|
||||
Want to write an app in your favorite language?! We'd be happy
|
||||
to add you to our [ecosystem](https://github.com/tendermint/awesome#ecosystem)!
|
||||
See [funding](https://github.com/interchainio/funding) opportunities from the
|
||||
[Interchain Foundation](https://interchain.io/) for implementations in new languages and more.
|
||||
|
||||
TODO link to bounties page.
|
||||
|
||||
The `abci-cli` is designed strictly for testing and debugging. In a real
|
||||
deployment, the role of sending messages is taken by Tendermint, which
|
||||
|
||||
@@ -4,4 +4,4 @@ order: false
|
||||
|
||||
# Validators
|
||||
|
||||
This file has moved to the [node section](../nodes/validators.md).
|
||||
_This file has moved to the [node section](../nodes/validators.md)._
|
||||
|
||||
@@ -23,8 +23,6 @@ yourself with the syntax.
|
||||
By following along with this guide, you'll create a Tendermint Core project
|
||||
called kvstore, a (very) simple distributed BFT key-value store.
|
||||
|
||||
> Note: please use a released version of Tendermint with this guide. The guides will work with the latest version. Please, do not use master.
|
||||
|
||||
## Built-in app vs external app
|
||||
|
||||
Running your application inside the same process as Tendermint Core will give
|
||||
@@ -36,28 +34,27 @@ through a TCP, Unix domain socket or gRPC.
|
||||
## 1.1 Installing Go
|
||||
|
||||
Please refer to [the official guide for installing
|
||||
Go](https://golang.org/doc/install).
|
||||
Go](https://go.dev/doc/install).
|
||||
|
||||
Verify that you have the latest version of Go installed:
|
||||
|
||||
```bash
|
||||
```sh
|
||||
$ go version
|
||||
go version go1.16.x darwin/amd64
|
||||
go version go1.18.x darwin/amd64
|
||||
```
|
||||
|
||||
## 1.2 Creating a new Go project
|
||||
|
||||
We'll start by creating a new Go project.
|
||||
We'll start by creating a new Go project. First, initialize the project folder with `go mod init`. Running this command should create the `go.mod` file.
|
||||
|
||||
```bash
|
||||
mkdir kvstore
|
||||
cd kvstore
|
||||
go mod init github.com/<github_username>/<repo_name>
|
||||
```sh
|
||||
$ mkdir kvstore
|
||||
$ cd kvstore
|
||||
$ go mod init github.com/<username>/kvstore
|
||||
go: creating new go.mod: module github.com/<username>/kvstore
|
||||
```
|
||||
|
||||
Inside the example directory create a `main.go` file with the following content:
|
||||
|
||||
> Note: there is no need to clone or fork Tendermint in this tutorial.
|
||||
Inside the project directory, create a `main.go` file with the following content:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -73,7 +70,7 @@ func main() {
|
||||
|
||||
When run, this should print "Hello, Tendermint Core" to the standard output.
|
||||
|
||||
```bash
|
||||
```sh
|
||||
$ go run main.go
|
||||
Hello, Tendermint Core
|
||||
```
|
||||
@@ -152,16 +149,43 @@ func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk
|
||||
}
|
||||
```
|
||||
|
||||
Now I will go through each method explaining when it's called and adding
|
||||
Now, we will go through each method and explain when it is executed while adding
|
||||
required business logic.
|
||||
|
||||
### 1.3.1 CheckTx
|
||||
### 1.3.1 Key-value store setup
|
||||
|
||||
When a new transaction is added to the Tendermint Core, it will ask the
|
||||
application to check it (validate the format, signatures, etc.).
|
||||
For the underlying key-value store we'll use the latest version of [badger](https://github.com/dgraph-io/badger), which is an embeddable, persistent and fast key-value (KV) database.
|
||||
|
||||
```sh
|
||||
$ go get github.com/dgraph-io/badger/v3
|
||||
go: added github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
```
|
||||
|
||||
```go
|
||||
import "bytes"
|
||||
import "github.com/dgraph-io/badger/v3"
|
||||
|
||||
type KVStoreApplication struct {
|
||||
db *badger.DB
|
||||
currentBatch *badger.Txn
|
||||
}
|
||||
|
||||
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
|
||||
return &KVStoreApplication{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.2 CheckTx
|
||||
|
||||
When a new transaction is added to the Tendermint Core, it will ask the application to check it (validate the format, signatures, etc.).
|
||||
|
||||
```go
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
...
|
||||
)
|
||||
|
||||
func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {
|
||||
// check format
|
||||
@@ -214,26 +238,7 @@ Valid transactions will eventually be committed given they are not too big and
|
||||
have enough gas. To learn more about gas, check out ["the
|
||||
specification"](https://github.com/tendermint/tendermint/blob/master/spec/abci/apps.md#gas).
|
||||
|
||||
For the underlying key-value store we'll use
|
||||
[badger](https://github.com/dgraph-io/badger), which is an embeddable,
|
||||
persistent and fast key-value (KV) database.
|
||||
|
||||
```go
|
||||
import "github.com/dgraph-io/badger"
|
||||
|
||||
type KVStoreApplication struct {
|
||||
db *badger.DB
|
||||
currentBatch *badger.Txn
|
||||
}
|
||||
|
||||
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
|
||||
return &KVStoreApplication{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit
|
||||
### 1.3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit
|
||||
|
||||
When Tendermint Core has decided on the block, it's transfered to the
|
||||
application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and
|
||||
@@ -290,7 +295,7 @@ func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.3 Query
|
||||
### 1.3.4 Query
|
||||
|
||||
Now, when the client wants to know whenever a particular key/value exist, it
|
||||
will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call
|
||||
@@ -348,17 +353,15 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/dgraph-io/badger"
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
tmflags "github.com/tendermint/tendermint/libs/cli/flags"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
nm "github.com/tendermint/tendermint/node"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
"github.com/tendermint/tendermint/proxy"
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abcitypes "github.com/tendermint/tendermint/abci/types"
|
||||
tmconfig "github.com/tendermint/tendermint/config"
|
||||
tmlog "github.com/tendermint/tendermint/libs/log"
|
||||
tmservice "github.com/tendermint/tendermint/libs/service"
|
||||
tmnode "github.com/tendermint/tendermint/node"
|
||||
)
|
||||
|
||||
var configFile string
|
||||
@@ -395,57 +398,42 @@ func main() {
|
||||
<-c
|
||||
}
|
||||
|
||||
func newTendermint(app abci.Application, configFile string) (*nm.Node, error) {
|
||||
// read config
|
||||
config := cfg.DefaultValidatorConfig()
|
||||
config.RootDir = filepath.Dir(filepath.Dir(configFile))
|
||||
viper.SetConfigFile(configFile)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to read config file: %w", err)
|
||||
}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
|
||||
}
|
||||
if err := config.ValidateBasic(); err != nil {
|
||||
return nil, fmt.Errorf("config is invalid: %w", err)
|
||||
}
|
||||
func newTendermint(app abcitypes.Application, configFile string) (tmservice.Service, error) {
|
||||
// read config
|
||||
config := tmconfig.DefaultValidatorConfig()
|
||||
config.SetRoot(filepath.Dir(filepath.Dir(configFile)))
|
||||
|
||||
// create logger
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
|
||||
var err error
|
||||
logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse log level: %w", err)
|
||||
}
|
||||
viper.SetConfigFile(configFile)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to read config file: %w", err)
|
||||
}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
|
||||
}
|
||||
if err := config.ValidateBasic(); err != nil {
|
||||
return nil, fmt.Errorf("config is invalid: %w", err)
|
||||
}
|
||||
|
||||
// read private validator
|
||||
pv := privval.LoadFilePV(
|
||||
config.PrivValidatorKeyFile(),
|
||||
config.PrivValidatorStateFile(),
|
||||
)
|
||||
// create logger
|
||||
logger, err := tmlog.NewDefaultLogger(tmlog.LogFormatPlain, config.LogLevel, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create logger: %w", err)
|
||||
}
|
||||
|
||||
// read node key
|
||||
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load node's key: %w", err)
|
||||
}
|
||||
// create node
|
||||
node, err := tmnode.New(
|
||||
config,
|
||||
logger,
|
||||
abciclient.NewLocalCreator(app),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
|
||||
}
|
||||
|
||||
// create node
|
||||
node, err := nm.NewNode(
|
||||
config,
|
||||
pv,
|
||||
nodeKey,
|
||||
abcicli.NewLocalClientCreator(app),
|
||||
nm.DefaultGenesisDocProviderFunc(config),
|
||||
nm.DefaultDBProvider,
|
||||
nm.DefaultMetricsProvider(config.Instrumentation),
|
||||
logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
|
||||
}
|
||||
|
||||
return node, nil
|
||||
return node, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This is a huge blob of code, so let's break it down into pieces.
|
||||
@@ -469,7 +457,7 @@ This can be avoided by setting the truncate option to true, like this:
|
||||
db, err := badger.Open(badger.DefaultOptions("/tmp/badger").WithTruncate(true))
|
||||
```
|
||||
|
||||
Then we use it to create a Tendermint Core `Node` instance:
|
||||
Then we use it to create a Tendermint Core [Service](https://github.com/tendermint/tendermint/blob/v0.35.8/libs/service/service.go#L24) instance:
|
||||
|
||||
```go
|
||||
flag.Parse()
|
||||
@@ -483,75 +471,48 @@ if err != nil {
|
||||
...
|
||||
|
||||
// create node
|
||||
node, err := nm.NewNode(
|
||||
config,
|
||||
pv,
|
||||
nodeKey,
|
||||
abcicli.NewLocalClientCreator(app),
|
||||
nm.DefaultGenesisDocProviderFunc(config),
|
||||
nm.DefaultDBProvider,
|
||||
nm.DefaultMetricsProvider(config.Instrumentation),
|
||||
logger)
|
||||
node, err := tmnode.New(
|
||||
config,
|
||||
logger,
|
||||
abciclient.NewLocalCreator(app),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
|
||||
return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
`NewNode` requires a few things including a configuration file, a private
|
||||
validator, a node key and a few others in order to construct the full node.
|
||||
[tmnode.New](https://github.com/tendermint/tendermint/blob/v0.35.8/node/public.go#L29) requires a few things including a configuration file, a logger and a few others in order to construct the full node.
|
||||
|
||||
Note we use `abcicli.NewLocalClientCreator` here to create a local client instead
|
||||
of one communicating through a socket or gRPC.
|
||||
Note that we use [abciclient.NewLocalCreator](https://github.com/tendermint/tendermint/blob/v0.35.8/abci/client/creators.go#L15) here to create a local client instead of one communicating through a socket or gRPC.
|
||||
|
||||
[viper](https://github.com/spf13/viper) is being used for reading the config,
|
||||
which we will generate later using the `tendermint init` command.
|
||||
|
||||
```go
|
||||
config := cfg.DefaultValidatorConfig()
|
||||
config.RootDir = filepath.Dir(filepath.Dir(configFile))
|
||||
// read config
|
||||
config := tmconfig.DefaultValidatorConfig()
|
||||
config.SetRoot(filepath.Dir(filepath.Dir(configFile)))
|
||||
viper.SetConfigFile(configFile)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to read config file: %w", err)
|
||||
return nil, fmt.Errorf("viper failed to read config file: %w", err)
|
||||
}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
|
||||
return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
|
||||
}
|
||||
if err := config.ValidateBasic(); err != nil {
|
||||
return nil, fmt.Errorf("config is invalid: %w", err)
|
||||
return nil, fmt.Errorf("config is invalid: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
We use `FilePV`, which is a private validator (i.e. thing which signs consensus
|
||||
messages). Normally, you would use `SignerRemote` to connect to an external
|
||||
[HSM](https://kb.certus.one/hsm.html).
|
||||
As for the logger, we use the built-in library, which provides a nice
|
||||
abstraction over [zerolog](https://github.com/rs/zerolog).
|
||||
|
||||
```go
|
||||
pv := privval.LoadFilePV(
|
||||
config.PrivValidatorKeyFile(),
|
||||
config.PrivValidatorStateFile(),
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
`nodeKey` is needed to identify the node in a p2p network.
|
||||
|
||||
```go
|
||||
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
|
||||
// create logger
|
||||
logger, err := tmlog.NewDefaultLogger(tmlog.LogFormatPlain, config.LogLevel, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load node's key: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
As for the logger, we use the build-in library, which provides a nice
|
||||
abstraction over [go-kit's
|
||||
logger](https://github.com/go-kit/kit/tree/master/log).
|
||||
|
||||
```go
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
|
||||
var err error
|
||||
logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse log level: %w", err)
|
||||
return nil, fmt.Errorf("failed to create logger: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -570,40 +531,41 @@ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
<-c
|
||||
```
|
||||
|
||||
## 1.5 Getting Up and Running
|
||||
## 1.5 Getting up and running
|
||||
|
||||
We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for
|
||||
dependency management.
|
||||
|
||||
```bash
|
||||
export GO111MODULE=on
|
||||
go mod init github.com/me/example
|
||||
```
|
||||
|
||||
This should create a `go.mod` file. The current tutorial only works with
|
||||
the master branch of Tendermint. so let's make sure we're using the latest version:
|
||||
Make sure to enable [Go modules](https://github.com/golang/go/wiki/Modules). Run `go mod tidy` to download and add dependencies in `go.mod` file.
|
||||
|
||||
```sh
|
||||
go get github.com/tendermint/tendermint@master
|
||||
$ go mod tidy
|
||||
...
|
||||
```
|
||||
|
||||
Let's make sure we're using the latest version of Tendermint (currently `v0.35.8`).
|
||||
|
||||
```sh
|
||||
$ go get github.com/tendermint/tendermint@latest
|
||||
...
|
||||
```
|
||||
|
||||
This will populate the `go.mod` with a release number followed by a hash for Tendermint.
|
||||
|
||||
```go
|
||||
module github.com/me/example
|
||||
module github.com/<username>/kvstore
|
||||
|
||||
go 1.15
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dgraph-io/badger v1.6.2
|
||||
github.com/tendermint/tendermint <vX>
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
github.com/tendermint/tendermint v0.35.8
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Now we can build the binary:
|
||||
Now, we can build the binary:
|
||||
|
||||
```bash
|
||||
go build
|
||||
```sh
|
||||
$ go build
|
||||
...
|
||||
```
|
||||
|
||||
To create a default configuration, nodeKey and private validator files, let's
|
||||
@@ -614,66 +576,87 @@ installing from source, don't forget to checkout the latest release (`git
|
||||
checkout vX.Y.Z`). Don't forget to check that the application uses the same
|
||||
major version.
|
||||
|
||||
```bash
|
||||
$ rm -rf /tmp/example
|
||||
$ TMHOME="/tmp/example" tendermint init validator
|
||||
```sh
|
||||
$ rm -rf /tmp/kvstore /tmp/badger
|
||||
$ TMHOME="/tmp/kvstore" tendermint init validator
|
||||
|
||||
I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json
|
||||
I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json
|
||||
I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json
|
||||
I[2019-07-16|18:40:36.483] Generated config module=main mode=validator
|
||||
2022-07-20T17:04:41+08:00 INFO Generated private validator keyFile=/tmp/kvstore/config/priv_validator_key.json module=main stateFile=/tmp/kvstore/data/priv_validator_state.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated node key module=main path=/tmp/kvstore/config/node_key.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated genesis file module=main path=/tmp/kvstore/config/genesis.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated config mode=validator module=main
|
||||
```
|
||||
|
||||
Feel free to explore the generated files, which can be found at
|
||||
`/tmp/kvstore/config` directory. Documentation on the config can be found
|
||||
[here](https://docs.tendermint.com/master/tendermint-core/configuration.html).
|
||||
|
||||
We are ready to start our application:
|
||||
|
||||
```bash
|
||||
$ ./example -config "/tmp/example/config/config.toml"
|
||||
```sh
|
||||
$ ./kvstore -config "/tmp/kvstore/config/config.toml"
|
||||
|
||||
badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s
|
||||
badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0
|
||||
badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s
|
||||
E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p
|
||||
I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0
|
||||
I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash=
|
||||
badger 2022/07/16 13:55:59 INFO: All 0 tables opened in 0s
|
||||
badger 2022/07/16 13:55:59 INFO: Replaying file id: 0 at offset: 0
|
||||
badger 2022/07/16 13:55:59 INFO: Replay took: 3.052µs
|
||||
badger 2022/07/16 13:55:59 DEBUG: Value log discard stats empty
|
||||
2022-07-16T13:55:59+08:00 INFO starting service impl=multiAppConn module=proxy service=multiAppConn
|
||||
2022-07-16T13:55:59+08:00 INFO starting service connection=query impl=localClient module=abci-client service=localClient
|
||||
2022-07-16T13:55:59+08:00 INFO starting service connection=snapshot impl=localClient module=abci-client service=localClient
|
||||
2022-07-16T13:55:59+08:00 INFO starting service connection=mempool impl=localClient module=abci-client service=localClient
|
||||
2022-07-16T13:55:59+08:00 INFO starting service connection=consensus impl=localClient module=abci-client service=localClient
|
||||
2022-07-16T13:55:59+08:00 INFO starting service impl=EventBus module=events service=EventBus
|
||||
2022-07-16T13:55:59+08:00 INFO starting service impl=PubSub module=pubsub service=PubSub
|
||||
2022-07-16T13:55:59+08:00 INFO starting service impl=IndexerService module=txindex service=IndexerService
|
||||
2022-07-16T13:55:59+08:00 INFO ABCI Handshake App Info hash= height=0 module=consensus protocol-version=0 software-version=
|
||||
2022-07-16T13:55:59+08:00 INFO ABCI Replay Blocks appHeight=0 module=consensus stateHeight=0 storeHeight=0
|
||||
2022-07-16T13:55:59+08:00 INFO Completed ABCI Handshake - Tendermint and App are synced appHash= appHeight=0 module=consensus
|
||||
2022-07-16T13:55:59+08:00 INFO Version info block=11 mode=validator p2p=8 tmVersion=0.35.8
|
||||
```
|
||||
|
||||
Now open another tab in your terminal and try sending a transaction:
|
||||
Let's try sending a transaction. Open another terminal and execute the below command.
|
||||
|
||||
```bash
|
||||
```sh
|
||||
$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'
|
||||
{
|
||||
"check_tx": {
|
||||
"gasWanted": "1",
|
||||
...
|
||||
},
|
||||
"deliver_tx": { ... },
|
||||
"hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",
|
||||
"height": "128"
|
||||
...
|
||||
"result": {
|
||||
"check_tx": {
|
||||
...
|
||||
"gas_wanted": "1",
|
||||
...
|
||||
},
|
||||
"deliver_tx": {...},
|
||||
"hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",
|
||||
"height": "91"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response should contain the height where this transaction was committed.
|
||||
|
||||
Now let's check if the given key now exists and its value:
|
||||
Let's check if the given key now exists and its value:
|
||||
|
||||
```json
|
||||
```sh
|
||||
$ curl -s 'localhost:26657/abci_query?data="tendermint"'
|
||||
{
|
||||
"response": {
|
||||
"code": 0,
|
||||
"log": "exists",
|
||||
"info": "",
|
||||
"index": "0",
|
||||
"key": "dGVuZGVybWludA==",
|
||||
"value": "cm9ja3M=",
|
||||
"proofOps": null,
|
||||
"height": "6",
|
||||
"codespace": ""
|
||||
...
|
||||
"result": {
|
||||
"response": {
|
||||
"code": 0,
|
||||
"log": "exists",
|
||||
"info": "",
|
||||
"index": "0",
|
||||
"key": "dGVuZGVybWludA==",
|
||||
"value": "cm9ja3M=",
|
||||
"proofOps": null,
|
||||
"height": "0",
|
||||
"codespace": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of
|
||||
`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of
|
||||
"tendermint" and "rocks" accordingly.
|
||||
|
||||
## Outro
|
||||
|
||||
@@ -37,25 +37,27 @@ Core will not have access to application's state.
|
||||
## 1.1 Installing Go
|
||||
|
||||
Please refer to [the official guide for installing
|
||||
Go](https://golang.org/doc/install).
|
||||
Go](https://go.dev/doc/install).
|
||||
|
||||
Verify that you have the latest version of Go installed:
|
||||
|
||||
```bash
|
||||
```sh
|
||||
$ go version
|
||||
go version go1.16.x darwin/amd64
|
||||
go version go1.18.x darwin/amd64
|
||||
```
|
||||
|
||||
## 1.2 Creating a new Go project
|
||||
|
||||
We'll start by creating a new Go project.
|
||||
We'll start by creating a new Go project. Initialize the folder with `go mod init`. Running this command should create the `go.mod` file.
|
||||
|
||||
```bash
|
||||
mkdir kvstore
|
||||
cd kvstore
|
||||
```sh
|
||||
$ mkdir kvstore
|
||||
$ cd kvstore
|
||||
$ go mod init github.com/<username>/kvstore
|
||||
go: creating new go.mod: module github.com/<username>/kvstore
|
||||
```
|
||||
|
||||
Inside the example directory create a `main.go` file with the following content:
|
||||
Inside the project directory, create a `main.go` file with the following content:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -71,8 +73,8 @@ func main() {
|
||||
|
||||
When run, this should print "Hello, Tendermint Core" to the standard output.
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```sh
|
||||
$ go run main.go
|
||||
Hello, Tendermint Core
|
||||
```
|
||||
|
||||
@@ -150,10 +152,34 @@ func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk
|
||||
}
|
||||
```
|
||||
|
||||
Now I will go through each method explaining when it's called and adding
|
||||
Now, we will go through each method and explain when it is executed while adding
|
||||
required business logic.
|
||||
|
||||
### 1.3.1 CheckTx
|
||||
### 1.3.1 Key-value store setup
|
||||
|
||||
For the underlying key-value store we'll use the latest version of [badger](https://github.com/dgraph-io/badger), which is an embeddable, persistent and fast key-value (KV) database.
|
||||
|
||||
```sh
|
||||
$ go get github.com/dgraph-io/badger/v3
|
||||
go: added github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/dgraph-io/badger/v3"
|
||||
|
||||
type KVStoreApplication struct {
|
||||
db *badger.DB
|
||||
currentBatch *badger.Txn
|
||||
}
|
||||
|
||||
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
|
||||
return &KVStoreApplication{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.2 CheckTx
|
||||
|
||||
When a new transaction is added to the Tendermint Core, it will ask the
|
||||
application to check it (validate the format, signatures, etc.).
|
||||
@@ -212,26 +238,8 @@ Valid transactions will eventually be committed given they are not too big and
|
||||
have enough gas. To learn more about gas, check out ["the
|
||||
specification"](https://github.com/tendermint/tendermint/blob/master/spec/abci/apps.md#gas).
|
||||
|
||||
For the underlying key-value store we'll use
|
||||
[badger](https://github.com/dgraph-io/badger), which is an embeddable,
|
||||
persistent and fast key-value (KV) database.
|
||||
|
||||
```go
|
||||
import "github.com/dgraph-io/badger"
|
||||
|
||||
type KVStoreApplication struct {
|
||||
db *badger.DB
|
||||
currentBatch *badger.Txn
|
||||
}
|
||||
|
||||
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
|
||||
return &KVStoreApplication{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit
|
||||
### 1.3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit
|
||||
|
||||
When Tendermint Core has decided on the block, it's transferred to the
|
||||
application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and
|
||||
@@ -287,7 +295,7 @@ func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3.3 Query
|
||||
### 1.3.4 Query
|
||||
|
||||
Now, when the client wants to know whenever a particular key/value exist, it
|
||||
will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call
|
||||
@@ -344,7 +352,7 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/dgraph-io/badger"
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
|
||||
abciserver "github.com/tendermint/tendermint/abci/server"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -353,7 +361,7 @@ import (
|
||||
var socketAddr string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address")
|
||||
flag.StringVar(&socketAddr, "socket-addr", "unix://kvstore.sock", "Unix domain socket address")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -426,40 +434,41 @@ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
<-c
|
||||
```
|
||||
|
||||
## 1.5 Getting Up and Running
|
||||
## 1.5 Getting up and running
|
||||
|
||||
We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for
|
||||
dependency management.
|
||||
|
||||
```bash
|
||||
export GO111MODULE=on
|
||||
go mod init github.com/me/example
|
||||
```
|
||||
|
||||
This should create a `go.mod` file. The current tutorial only works with
|
||||
the master branch of Tendermint, so let's make sure we're using the latest version:
|
||||
Make sure to enable [Go modules](https://github.com/golang/go/wiki/Modules). Run `go mod tidy` to download and add dependencies in `go.mod` file.
|
||||
|
||||
```sh
|
||||
go get github.com/tendermint/tendermint@97a3e44e0724f2017079ce24d36433f03124c09e
|
||||
$ go mod tidy
|
||||
...
|
||||
```
|
||||
|
||||
Let's make sure we're using the latest version of Tendermint (currently `v0.35.8`).
|
||||
|
||||
```sh
|
||||
$ go get github.com/tendermint/tendermint@latest
|
||||
...
|
||||
```
|
||||
|
||||
This will populate the `go.mod` with a release number followed by a hash for Tendermint.
|
||||
|
||||
```go
|
||||
module github.com/me/example
|
||||
module github.com/<username>/kvstore
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dgraph-io/badger v1.6.2
|
||||
github.com/tendermint/tendermint <vX>
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
github.com/tendermint/tendermint v0.35.8
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Now we can build the binary:
|
||||
Now, we can build the binary:
|
||||
|
||||
```bash
|
||||
go build
|
||||
```sh
|
||||
$ go build
|
||||
...
|
||||
```
|
||||
|
||||
To create a default configuration, nodeKey and private validator files, let's
|
||||
@@ -470,94 +479,112 @@ installing from source, don't forget to checkout the latest release (`git
|
||||
checkout vX.Y.Z`). Don't forget to check that the application uses the same
|
||||
major version.
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/example
|
||||
TMHOME="/tmp/example" tendermint init validator
|
||||
```sh
|
||||
$ rm -rf /tmp/kvstore /tmp/badger
|
||||
$ TMHOME="/tmp/kvstore" tendermint init validator
|
||||
|
||||
I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json
|
||||
I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json
|
||||
I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json
|
||||
I[2019-07-16|18:20:36.483] Generated config module=main mode=validator
|
||||
2022-07-20T17:04:41+08:00 INFO Generated private validator keyFile=/tmp/kvstore/config/priv_validator_key.json module=main stateFile=/tmp/kvstore/data/priv_validator_state.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated node key module=main path=/tmp/kvstore/config/node_key.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated genesis file module=main path=/tmp/kvstore/config/genesis.json
|
||||
2022-07-20T17:04:41+08:00 INFO Generated config mode=validator module=main
|
||||
```
|
||||
|
||||
Feel free to explore the generated files, which can be found at
|
||||
`/tmp/example/config` directory. Documentation on the config can be found
|
||||
`/tmp/kvstore/config` directory. Documentation on the config can be found
|
||||
[here](https://docs.tendermint.com/master/tendermint-core/configuration.html).
|
||||
|
||||
We are ready to start our application:
|
||||
|
||||
```bash
|
||||
rm example.sock
|
||||
./example
|
||||
```sh
|
||||
$ rm kvstore.sock
|
||||
$ ./kvstore
|
||||
|
||||
badger 2019/07/16 18:25:11 INFO: All 0 tables opened in 0s
|
||||
badger 2019/07/16 18:25:11 INFO: Replaying file id: 0 at offset: 0
|
||||
badger 2019/07/16 18:25:11 INFO: Replay took: 300.4s
|
||||
I[2019-07-16|18:25:11.523] Starting ABCIServer impl=ABCIServ
|
||||
badger 2022/07/20 17:07:17 INFO: All 1 tables opened in 9ms
|
||||
badger 2022/07/20 17:07:17 INFO: Replaying file id: 0 at offset: 256
|
||||
badger 2022/07/20 17:07:17 INFO: Replay took: 9.077µs
|
||||
badger 2022/07/20 17:07:17 DEBUG: Value log discard stats empty
|
||||
2022-07-20T17:07:17+08:00 INFO starting service impl=ABCIServer service=ABCIServer
|
||||
2022-07-20T17:07:17+08:00 INFO Waiting for new connection...
|
||||
```
|
||||
|
||||
Then we need to start Tendermint Core and point it to our application. Staying
|
||||
within the application directory execute:
|
||||
Then, we need to start Tendermint Core and point it to our application. Staying
|
||||
within the project directory, open another terminal and execute the command below:
|
||||
|
||||
```bash
|
||||
TMHOME="/tmp/example" tendermint node --proxy-app=unix://example.sock
|
||||
```sh
|
||||
$ TMHOME="/tmp/kvstore" tendermint node --proxy-app=unix://kvstore.sock
|
||||
|
||||
I[2019-07-16|18:26:20.362] Version info module=main software=0.32.1 block=10 p2p=7
|
||||
I[2019-07-16|18:26:20.383] Starting Node module=main impl=Node
|
||||
E[2019-07-16|18:26:20.392] Couldn't connect to any seeds module=p2p
|
||||
I[2019-07-16|18:26:20.394] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:8dab80770ae8e295d4ce905d86af78c4ff634b79 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-nIO96P Version:0.32.1 Channels:4020212223303800 Moniker:app48.fun-box.ru Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}"
|
||||
I[2019-07-16|18:26:21.440] Executed block module=state height=1 validTxs=0 invalidTxs=0
|
||||
I[2019-07-16|18:26:21.446] Committed state module=state height=1 txs=0 appHash=
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=multiAppConn module=proxy service=multiAppConn
|
||||
2022-07-20T17:10:22+08:00 INFO starting service connection=query impl=socketClient module=abci-client service=socketClient
|
||||
2022-07-20T17:10:22+08:00 INFO starting service connection=snapshot impl=socketClient module=abci-client service=socketClient
|
||||
2022-07-20T17:10:22+08:00 INFO starting service connection=mempool impl=socketClient module=abci-client service=socketClient
|
||||
2022-07-20T17:10:22+08:00 INFO starting service connection=consensus impl=socketClient module=abci-client service=socketClient
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=EventBus module=events service=EventBus
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=PubSub module=pubsub service=PubSub
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=IndexerService module=txindex service=IndexerService
|
||||
...
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=Node module=main service=Node
|
||||
2022-07-20T17:10:22+08:00 INFO Starting RPC HTTP server on 127.0.0.1:26657 module=rpc-server
|
||||
2022-07-20T17:10:22+08:00 INFO p2p service legacy_enabled=false module=main
|
||||
2022-07-20T17:10:22+08:00 INFO starting service impl=router module=p2p service=router
|
||||
2022-07-20T17:10:22+08:00 INFO starting router channels=402021222330386061626300 listen_addr=tcp://0.0.0.0:26656 module=p2p net_addr={"id":"715727499e94f8fcaef1763192ebcc8460f44666","ip":"0.0.0.0","port":26656} node_id=715727499e94f8fcaef1763192ebcc8460f44666
|
||||
...
|
||||
```
|
||||
|
||||
This should start the full node and connect to our ABCI application.
|
||||
|
||||
```sh
|
||||
I[2019-07-16|18:25:11.525] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.329] Accepted a new connection
|
||||
I[2019-07-16|18:26:20.329] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.330] Accepted a new connection
|
||||
I[2019-07-16|18:26:20.330] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.330] Accepted a new connection
|
||||
2022-07-20T17:09:55+08:00 INFO Waiting for new connection...
|
||||
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
|
||||
2022-07-20T17:10:22+08:00 INFO Waiting for new connection...
|
||||
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
|
||||
2022-07-20T17:10:22+08:00 INFO Waiting for new connection...
|
||||
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
|
||||
```
|
||||
|
||||
Now open another tab in your terminal and try sending a transaction:
|
||||
Let's try sending a transaction. Open another terminal and execute the below command.
|
||||
|
||||
```json
|
||||
```sh
|
||||
$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'
|
||||
{
|
||||
"check_tx": {
|
||||
"gasWanted": "1",
|
||||
...
|
||||
},
|
||||
"deliver_tx": { ... },
|
||||
"hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB",
|
||||
"height": "33"
|
||||
...
|
||||
"result": {
|
||||
"check_tx": {
|
||||
...
|
||||
"gas_wanted": "1",
|
||||
...
|
||||
},
|
||||
"deliver_tx": { ... },
|
||||
"hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",
|
||||
"height": "15"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response should contain the height where this transaction was committed.
|
||||
|
||||
Now let's check if the given key now exists and its value:
|
||||
Let's check if the given key now exists and its value:
|
||||
|
||||
```json
|
||||
```sh
|
||||
$ curl -s 'localhost:26657/abci_query?data="tendermint"'
|
||||
{
|
||||
"response": {
|
||||
"code": 0,
|
||||
"log": "exists",
|
||||
"info": "",
|
||||
"index": "0",
|
||||
"key": "dGVuZGVybWludA==",
|
||||
"value": "cm9ja3M=",
|
||||
"proofOps": null,
|
||||
"height": "6",
|
||||
"codespace": ""
|
||||
...
|
||||
"result": {
|
||||
"response": {
|
||||
"code": 0,
|
||||
"log": "exists",
|
||||
"info": "",
|
||||
"index": "0",
|
||||
"key": "dGVuZGVybWludA==",
|
||||
"value": "cm9ja3M=",
|
||||
"proofOps": null,
|
||||
"height": "0",
|
||||
"codespace": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of
|
||||
`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of
|
||||
"tendermint" and "rocks" accordingly.
|
||||
|
||||
## Outro
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
master master
|
||||
v0.33.x v0.33
|
||||
v0.34.x v0.34
|
||||
v0.35.x v0.35
|
||||
master master
|
||||
|
||||
8
go.mod
8
go.mod
@@ -40,7 +40,7 @@ require (
|
||||
github.com/bufbuild/buf v1.4.0
|
||||
github.com/creachadair/atomicfile v0.2.6
|
||||
github.com/creachadair/taskgroup v0.3.2
|
||||
github.com/golangci/golangci-lint v1.47.1
|
||||
github.com/golangci/golangci-lint v1.47.2
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/vektra/mockery/v2 v2.14.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
@@ -240,8 +240,12 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/creachadair/tomledit v0.0.22
|
||||
github.com/creachadair/tomledit v0.0.23
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.37.0
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca
|
||||
)
|
||||
|
||||
retract (
|
||||
[v0.35.0,v0.35.9] // See https://github.com/tendermint/tendermint/discussions/9155
|
||||
)
|
||||
8
go.sum
8
go.sum
@@ -245,8 +245,8 @@ github.com/creachadair/atomicfile v0.2.6/go.mod h1:BRq8Une6ckFneYXZQ+kO7p1ZZP3I2
|
||||
github.com/creachadair/command v0.0.0-20220426235536-a748effdf6a1/go.mod h1:bAM+qFQb/KwWyCc9MLC4U1jvn3XyakqP5QRkds5T6cY=
|
||||
github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM=
|
||||
github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk=
|
||||
github.com/creachadair/tomledit v0.0.22 h1:lRtepmrwhzDq+g1gv5ftVn5itgo7CjYbm6abKTToqJ4=
|
||||
github.com/creachadair/tomledit v0.0.22/go.mod h1:cIu/4x5L855oSRejIqr+WRFh+mv9g4fWLiUFaApYn/Y=
|
||||
github.com/creachadair/tomledit v0.0.23 h1:ohYJjMsxwzj4dDzKaBWFbWH5J+3LO/8CYnlVY+baBWA=
|
||||
github.com/creachadair/tomledit v0.0.23/go.mod h1:cIu/4x5L855oSRejIqr+WRFh+mv9g4fWLiUFaApYn/Y=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -449,8 +449,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6
|
||||
github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ=
|
||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
|
||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.47.1 h1:hbubHskV2Ppwz4ZZE2lc0/Pw9ZhqLuzm2dT7ZVpLA6Y=
|
||||
github.com/golangci/golangci-lint v1.47.1/go.mod h1:lpS2pjBZtRyXewUcOY7yUL3K4KfpoWz072yRN8AuhHg=
|
||||
github.com/golangci/golangci-lint v1.47.2 h1:qvMDVv49Hrx3PSEXZ0bD/yhwSbhsOihQjFYCKieegIw=
|
||||
github.com/golangci/golangci-lint v1.47.2/go.mod h1:lpS2pjBZtRyXewUcOY7yUL3K4KfpoWz072yRN8AuhHg=
|
||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
|
||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||
|
||||
@@ -469,12 +469,10 @@ func (r *Reactor) poolRoutine(ctx context.Context, stateSynced bool, blockSyncCh
|
||||
lastAdvance = r.pool.LastAdvance()
|
||||
)
|
||||
|
||||
r.logger.Debug(
|
||||
"consensus ticker",
|
||||
r.logger.Debug("consensus ticker",
|
||||
"num_pending", numPending,
|
||||
"total", lenRequesters,
|
||||
"height", height,
|
||||
)
|
||||
"height", height)
|
||||
|
||||
switch {
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ import (
|
||||
// Byzantine node sends two different prevotes (nil and blockID) to the same
|
||||
// validator.
|
||||
func TestByzantinePrevoteEquivocation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// empirically, this test either passes in <1s or hits some
|
||||
// kind of deadlock and hit the larger timeout. This timeout
|
||||
// can be extended a bunch if needed, but it's good to avoid
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
cstypes "github.com/tendermint/tendermint/internal/consensus/types"
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/bits"
|
||||
@@ -1113,7 +1114,7 @@ func (r *Reactor) handleDataMessage(ctx context.Context, envelope *p2p.Envelope,
|
||||
}
|
||||
|
||||
if r.WaitSync() {
|
||||
logger.Info("ignoring message received during sync", "msg", fmt.Sprintf("%T", msgI))
|
||||
logger.Info("ignoring message received during sync", "msg", tmstrings.LazySprintf("%T", msgI))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -779,6 +779,10 @@ func TestReactorRecordsVotesAndBlockParts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReactorVotingPowerChange(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -885,6 +889,10 @@ func TestReactorVotingPowerChange(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReactorValidatorSetChanges(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ func sendTxs(ctx context.Context, t *testing.T, cs *State) {
|
||||
|
||||
// TestWALCrash uses crashing WAL to test we can recover from any WAL failure.
|
||||
func TestWALCrash(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
initFn func(dbm.DB, *State, context.Context)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/tendermint/tendermint/internal/eventbus"
|
||||
"github.com/tendermint/tendermint/internal/jsontypes"
|
||||
"github.com/tendermint/tendermint/internal/libs/autofile"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
tmevents "github.com/tendermint/tendermint/libs/events"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -778,11 +779,9 @@ func (cs *State) updateToState(state sm.State) {
|
||||
// signal the new round step, because other services (eg. txNotifier)
|
||||
// depend on having an up-to-date peer state!
|
||||
if state.LastBlockHeight <= cs.state.LastBlockHeight {
|
||||
cs.logger.Debug(
|
||||
"ignoring updateToState()",
|
||||
cs.logger.Debug("ignoring updateToState()",
|
||||
"new_height", state.LastBlockHeight+1,
|
||||
"old_height", cs.state.LastBlockHeight+1,
|
||||
)
|
||||
"old_height", cs.state.LastBlockHeight+1)
|
||||
cs.newStep()
|
||||
return
|
||||
}
|
||||
@@ -1038,12 +1037,10 @@ func (cs *State) handleMsg(ctx context.Context, mi msgInfo) {
|
||||
}
|
||||
|
||||
if err != nil && msg.Round != cs.Round {
|
||||
cs.logger.Debug(
|
||||
"received block part from wrong round",
|
||||
cs.logger.Debug("received block part from wrong round",
|
||||
"height", cs.Height,
|
||||
"cs_round", cs.Round,
|
||||
"block_round", msg.Round,
|
||||
)
|
||||
"block_round", msg.Round)
|
||||
err = nil
|
||||
}
|
||||
|
||||
@@ -1073,7 +1070,7 @@ func (cs *State) handleMsg(ctx context.Context, mi msgInfo) {
|
||||
// We could make note of this and help filter in broadcastHasVoteMessage().
|
||||
|
||||
default:
|
||||
cs.logger.Error("unknown msg type", "type", fmt.Sprintf("%T", msg))
|
||||
cs.logger.Error("unknown msg type", "type", tmstrings.LazySprintf("%T", msg))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1184,10 +1181,10 @@ func (cs *State) enterNewRound(ctx context.Context, height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cs.Step != cstypes.RoundStepNewHeight) {
|
||||
logger.Debug(
|
||||
"entering new round with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering new round with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1195,7 +1192,10 @@ func (cs *State) enterNewRound(ctx context.Context, height int64, round int32) {
|
||||
logger.Debug("need to set a buffer and log message here for sanity", "start_time", cs.StartTime, "now", now)
|
||||
}
|
||||
|
||||
logger.Debug("entering new round", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering new round",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
// increment validators if necessary
|
||||
validators := cs.Validators
|
||||
@@ -1274,10 +1274,10 @@ func (cs *State) enterPropose(ctx context.Context, height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPropose <= cs.Step) {
|
||||
logger.Debug(
|
||||
"entering propose step with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering propose step with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1291,7 +1291,10 @@ func (cs *State) enterPropose(ctx context.Context, height int64, round int32) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("entering propose step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering propose step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
defer func() {
|
||||
// Done enterPropose:
|
||||
@@ -1333,17 +1336,13 @@ func (cs *State) enterPropose(ctx context.Context, height int64, round int32) {
|
||||
}
|
||||
|
||||
if cs.isProposer(addr) {
|
||||
logger.Debug(
|
||||
"propose step; our turn to propose",
|
||||
"proposer", addr,
|
||||
)
|
||||
logger.Debug("propose step; our turn to propose",
|
||||
"proposer", addr)
|
||||
|
||||
cs.decideProposal(ctx, height, round)
|
||||
} else {
|
||||
logger.Debug(
|
||||
"propose step; not our turn to propose",
|
||||
"proposer", cs.Validators.GetProposer().Address,
|
||||
)
|
||||
logger.Debug("propose step; not our turn to propose",
|
||||
"proposer", cs.Validators.GetProposer().Address)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1480,10 +1479,10 @@ func (cs *State) enterPrevote(ctx context.Context, height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrevote <= cs.Step) {
|
||||
logger.Debug(
|
||||
"entering prevote step with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering prevote step with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1493,7 +1492,10 @@ func (cs *State) enterPrevote(ctx context.Context, height int64, round int32) {
|
||||
cs.newStep()
|
||||
}()
|
||||
|
||||
logger.Debug("entering prevote step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering prevote step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
// Sign and broadcast vote as necessary
|
||||
cs.doPrevote(ctx, height, round)
|
||||
@@ -1533,14 +1535,10 @@ func (cs *State) defaultDoPrevote(ctx context.Context, height int64, round int32
|
||||
//TODO: Remove this temporary fix when the complete solution is ready. See #8739
|
||||
if !cs.replayMode && cs.Proposal.POLRound == -1 && cs.LockedRound == -1 && !cs.proposalIsTimely() {
|
||||
logger.Debug("prevote step: Proposal is not timely; prevoting nil",
|
||||
"proposed",
|
||||
tmtime.Canonical(cs.Proposal.Timestamp).Format(time.RFC3339Nano),
|
||||
"received",
|
||||
tmtime.Canonical(cs.ProposalReceiveTime).Format(time.RFC3339Nano),
|
||||
"msg_delay",
|
||||
sp.MessageDelay,
|
||||
"precision",
|
||||
sp.Precision)
|
||||
"proposed", tmtime.Canonical(cs.Proposal.Timestamp).Format(time.RFC3339Nano),
|
||||
"received", tmtime.Canonical(cs.ProposalReceiveTime).Format(time.RFC3339Nano),
|
||||
"msg_delay", sp.MessageDelay,
|
||||
"precision", sp.Precision)
|
||||
cs.signAddVote(ctx, tmproto.PrevoteType, nil, types.PartSetHeader{})
|
||||
return
|
||||
}
|
||||
@@ -1625,8 +1623,8 @@ func (cs *State) defaultDoPrevote(ctx context.Context, height int64, round int32
|
||||
blockID, ok := cs.Votes.Prevotes(cs.Proposal.POLRound).TwoThirdsMajority()
|
||||
if ok && cs.ProposalBlock.HashesTo(blockID.Hash) && cs.Proposal.POLRound >= 0 && cs.Proposal.POLRound < cs.Round {
|
||||
if cs.LockedRound <= cs.Proposal.POLRound {
|
||||
logger.Debug("prevote step: ProposalBlock is valid and received a 2/3" +
|
||||
"majority in a round later than the locked round; prevoting the proposal")
|
||||
logger.Debug("prevote step: ProposalBlock is valid and received a 2/3 majority in a round later than the locked round",
|
||||
"outcome", "prevoting the proposal")
|
||||
cs.signAddVote(ctx, tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header())
|
||||
return
|
||||
}
|
||||
@@ -1637,8 +1635,8 @@ func (cs *State) defaultDoPrevote(ctx context.Context, height int64, round int32
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("prevote step: ProposalBlock is valid but was not our locked block or " +
|
||||
"did not receive a more recent majority; prevoting nil")
|
||||
logger.Debug("prevote step: ProposalBlock is valid but was not our locked block or did not receive a more recent majority",
|
||||
"outcome", "prevoting nil")
|
||||
cs.signAddVote(ctx, tmproto.PrevoteType, nil, types.PartSetHeader{})
|
||||
}
|
||||
|
||||
@@ -1647,10 +1645,10 @@ func (cs *State) enterPrevoteWait(height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrevoteWait <= cs.Step) {
|
||||
logger.Debug(
|
||||
"entering prevote wait step with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering prevote wait step with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1661,7 +1659,10 @@ func (cs *State) enterPrevoteWait(height int64, round int32) {
|
||||
))
|
||||
}
|
||||
|
||||
logger.Debug("entering prevote wait step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering prevote wait step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
defer func() {
|
||||
// Done enterPrevoteWait:
|
||||
@@ -1679,17 +1680,21 @@ func (cs *State) enterPrevoteWait(height int64, round int32) {
|
||||
// Lock & precommit the ProposalBlock if we have enough prevotes for it (a POL in this round)
|
||||
// else, precommit nil otherwise.
|
||||
func (cs *State) enterPrecommit(ctx context.Context, height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
logger := cs.logger.With("new_height", height, "new_round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrecommit <= cs.Step) {
|
||||
logger.Debug(
|
||||
"entering precommit step with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering precommit step with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("entering precommit step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering precommit step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
defer func() {
|
||||
// Done enterPrecommit:
|
||||
@@ -1796,14 +1801,13 @@ func (cs *State) enterPrecommit(ctx context.Context, height int64, round int32)
|
||||
|
||||
// Enter: any +2/3 precommits for next round.
|
||||
func (cs *State) enterPrecommitWait(height int64, round int32) {
|
||||
logger := cs.logger.With("height", height, "round", round)
|
||||
logger := cs.logger.With("new_height", height, "new_round", round)
|
||||
|
||||
if cs.Height != height || round < cs.Round || (cs.Round == round && cs.TriggeredTimeoutPrecommit) {
|
||||
logger.Debug(
|
||||
"entering precommit wait step with invalid args",
|
||||
logger.Debug("entering precommit wait step with invalid args",
|
||||
"triggered_timeout", cs.TriggeredTimeoutPrecommit,
|
||||
"current", fmt.Sprintf("%v/%v", cs.Height, cs.Round),
|
||||
)
|
||||
"height", cs.Height,
|
||||
"round", cs.Round)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1814,7 +1818,10 @@ func (cs *State) enterPrecommitWait(height int64, round int32) {
|
||||
))
|
||||
}
|
||||
|
||||
logger.Debug("entering precommit wait step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering precommit wait step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
defer func() {
|
||||
// Done enterPrecommitWait:
|
||||
@@ -1828,17 +1835,20 @@ func (cs *State) enterPrecommitWait(height int64, round int32) {
|
||||
|
||||
// Enter: +2/3 precommits for block
|
||||
func (cs *State) enterCommit(ctx context.Context, height int64, commitRound int32) {
|
||||
logger := cs.logger.With("height", height, "commit_round", commitRound)
|
||||
logger := cs.logger.With("new_height", height, "commit_round", commitRound)
|
||||
|
||||
if cs.Height != height || cstypes.RoundStepCommit <= cs.Step {
|
||||
logger.Debug(
|
||||
"entering commit step with invalid args",
|
||||
"current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step),
|
||||
)
|
||||
logger.Debug("entering commit step with invalid args",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("entering commit step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
|
||||
logger.Debug("entering commit step",
|
||||
"height", cs.Height,
|
||||
"round", cs.Round,
|
||||
"step", cs.Step)
|
||||
|
||||
defer func() {
|
||||
// Done enterCommit:
|
||||
@@ -1892,12 +1902,12 @@ func (cs *State) enterCommit(ctx context.Context, height int64, commitRound int3
|
||||
|
||||
// If we have the block AND +2/3 commits for it, finalize.
|
||||
func (cs *State) tryFinalizeCommit(ctx context.Context, height int64) {
|
||||
logger := cs.logger.With("height", height)
|
||||
|
||||
if cs.Height != height {
|
||||
panic(fmt.Sprintf("tryFinalizeCommit() cs.Height: %v vs height: %v", cs.Height, height))
|
||||
}
|
||||
|
||||
logger := cs.logger.With("height", height)
|
||||
|
||||
blockID, ok := cs.Votes.Precommits(cs.CommitRound).TwoThirdsMajority()
|
||||
if !ok || blockID.IsNil() {
|
||||
logger.Error("failed attempt to finalize commit; there was no +2/3 majority or +2/3 was for nil")
|
||||
@@ -1907,9 +1917,8 @@ func (cs *State) tryFinalizeCommit(ctx context.Context, height int64) {
|
||||
if !cs.ProposalBlock.HashesTo(blockID.Hash) {
|
||||
// TODO: this happens every time if we're not a validator (ugly logs)
|
||||
// TODO: ^^ wait, why does it matter that we're a validator?
|
||||
logger.Debug(
|
||||
"failed attempt to finalize commit; we do not have the commit block",
|
||||
"proposal_block", cs.ProposalBlock.Hash(),
|
||||
logger.Debug("failed attempt to finalize commit; we do not have the commit block",
|
||||
"proposal_block", tmstrings.LazyBlockHash(cs.ProposalBlock),
|
||||
"commit_block", blockID.Hash,
|
||||
)
|
||||
return
|
||||
@@ -1951,11 +1960,10 @@ func (cs *State) finalizeCommit(ctx context.Context, height int64) {
|
||||
|
||||
logger.Info(
|
||||
"finalizing commit of block",
|
||||
"hash", block.Hash(),
|
||||
"hash", tmstrings.LazyBlockHash(block),
|
||||
"root", block.AppHash,
|
||||
"num_txs", len(block.Txs),
|
||||
)
|
||||
logger.Debug(fmt.Sprintf("%v", block))
|
||||
|
||||
// Save to blockStore.
|
||||
if cs.blockStore.Height() < block.Height {
|
||||
@@ -2052,8 +2060,11 @@ func (cs *State) RecordMetrics(height int64, block *types.Block) {
|
||||
address types.Address
|
||||
)
|
||||
if commitSize != valSetLen {
|
||||
cs.logger.Error(fmt.Sprintf("commit size (%d) doesn't match valset length (%d) at height %d\n\n%v\n\n%v",
|
||||
commitSize, valSetLen, block.Height, block.LastCommit.Signatures, cs.LastValidators.Validators))
|
||||
cs.logger.Error("commit size doesn't match valset",
|
||||
"size", commitSize,
|
||||
"valset_len", valSetLen,
|
||||
"height", block.Height,
|
||||
"extra", tmstrings.LazySprintf("\n%v\n\n%v", block.LastCommit.Signatures, cs.LastValidators.Validators))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2187,8 +2198,7 @@ func (cs *State) addProposalBlockPart(
|
||||
cs.metrics.BlockGossipPartsReceived.With("matches_current", "false").Add(1)
|
||||
// NOTE: this can happen when we've gone to a higher round and
|
||||
// then receive parts from the previous round - not necessarily a bad peer.
|
||||
cs.logger.Debug(
|
||||
"received a block part when we are not expecting any",
|
||||
cs.logger.Debug("received a block part when we are not expecting any",
|
||||
"height", height,
|
||||
"round", round,
|
||||
"index", part.Index,
|
||||
@@ -2248,11 +2258,9 @@ func (cs *State) handleCompleteProposal(ctx context.Context, height int64) {
|
||||
blockID, hasTwoThirds := prevotes.TwoThirdsMajority()
|
||||
if hasTwoThirds && !blockID.IsNil() && (cs.ValidRound < cs.Round) {
|
||||
if cs.ProposalBlock.HashesTo(blockID.Hash) {
|
||||
cs.logger.Debug(
|
||||
"updating valid block to new proposal block",
|
||||
cs.logger.Debug("updating valid block to new proposal block",
|
||||
"valid_round", cs.Round,
|
||||
"valid_block_hash", cs.ProposalBlock.Hash(),
|
||||
)
|
||||
"valid_block_hash", tmstrings.LazyBlockHash(cs.ProposalBlock))
|
||||
|
||||
cs.ValidRound = cs.Round
|
||||
cs.ValidBlock = cs.ProposalBlock
|
||||
@@ -2291,23 +2299,19 @@ func (cs *State) tryAddVote(ctx context.Context, vote *types.Vote, peerID types.
|
||||
}
|
||||
|
||||
if bytes.Equal(vote.ValidatorAddress, cs.privValidatorPubKey.Address()) {
|
||||
cs.logger.Error(
|
||||
"found conflicting vote from ourselves; did you unsafe_reset a validator?",
|
||||
cs.logger.Error("found conflicting vote from ourselves; did you unsafe_reset a validator?",
|
||||
"height", vote.Height,
|
||||
"round", vote.Round,
|
||||
"type", vote.Type,
|
||||
)
|
||||
"type", vote.Type)
|
||||
|
||||
return added, err
|
||||
}
|
||||
|
||||
// report conflicting votes to the evidence pool
|
||||
cs.evpool.ReportConflictingVotes(voteErr.VoteA, voteErr.VoteB)
|
||||
cs.logger.Debug(
|
||||
"found and sent conflicting votes to the evidence pool",
|
||||
cs.logger.Debug("found and sent conflicting votes to the evidence pool",
|
||||
"vote_a", voteErr.VoteA,
|
||||
"vote_b", voteErr.VoteB,
|
||||
)
|
||||
"vote_b", voteErr.VoteB)
|
||||
|
||||
return added, err
|
||||
} else if errors.Is(err, types.ErrVoteNonDeterministicSignature) {
|
||||
@@ -2331,13 +2335,11 @@ func (cs *State) addVote(
|
||||
vote *types.Vote,
|
||||
peerID types.NodeID,
|
||||
) (added bool, err error) {
|
||||
cs.logger.Debug(
|
||||
"adding vote",
|
||||
cs.logger.Debug("adding vote",
|
||||
"vote_height", vote.Height,
|
||||
"vote_type", vote.Type,
|
||||
"val_index", vote.ValidatorIndex,
|
||||
"cs_height", cs.Height,
|
||||
)
|
||||
"cs_height", cs.Height)
|
||||
|
||||
if vote.Height < cs.Height || (vote.Height == cs.Height && vote.Round < cs.Round) {
|
||||
cs.metrics.MarkLateVote(vote.Type)
|
||||
@@ -2458,11 +2460,9 @@ func (cs *State) addVote(
|
||||
cs.ValidBlock = cs.ProposalBlock
|
||||
cs.ValidBlockParts = cs.ProposalBlockParts
|
||||
} else {
|
||||
cs.logger.Debug(
|
||||
"valid block we do not know about; set ProposalBlock=nil",
|
||||
"proposal", cs.ProposalBlock.Hash(),
|
||||
"block_id", blockID.Hash,
|
||||
)
|
||||
cs.logger.Debug("valid block we do not know about; set ProposalBlock=nil",
|
||||
"proposal", tmstrings.LazyBlockHash(cs.ProposalBlock),
|
||||
"block_id", blockID.Hash)
|
||||
|
||||
// we're getting the wrong block
|
||||
cs.ProposalBlock = nil
|
||||
|
||||
@@ -3,8 +3,94 @@ package strings
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
)
|
||||
|
||||
type lazyStringf struct {
|
||||
tmpl string
|
||||
args []interface{}
|
||||
out string
|
||||
}
|
||||
|
||||
func (s *lazyStringf) String() string {
|
||||
if s.out == "" && s.tmpl != "" {
|
||||
s.out = fmt.Sprintf(s.tmpl, s.args)
|
||||
s.args = nil
|
||||
s.tmpl = ""
|
||||
}
|
||||
return s.out
|
||||
}
|
||||
|
||||
// LazySprintf creates a fmt.Stringer implementation with similar
|
||||
// semantics as fmt.Sprintf, *except* that the string is built when
|
||||
// String() is called on the object. This means that format arguments
|
||||
// are resolved/captured into string format when String() is called,
|
||||
// and not, as in fmt.Sprintf when that function returns.
|
||||
//
|
||||
// As a result, if you use this type in go routines or defer
|
||||
// statements it's possible to pass an argument to LazySprintf which
|
||||
// has one value at the call site and a different value when the
|
||||
// String() is evaluated, which may lead to unexpected outcomes. In
|
||||
// these situations, either be *extremely* careful about the arguments
|
||||
// passed to this function or use fmt.Sprintf.
|
||||
//
|
||||
// The implementation also caches the output of the underlying
|
||||
// fmt.Sprintf statement when String() is called, so subsequent calls
|
||||
// will produce the same result.
|
||||
func LazySprintf(t string, args ...interface{}) fmt.Stringer {
|
||||
return &lazyStringf{tmpl: t, args: args}
|
||||
}
|
||||
|
||||
type lazyStringer struct {
|
||||
val fmt.Stringer
|
||||
out string
|
||||
}
|
||||
|
||||
func (l *lazyStringer) String() string {
|
||||
if l.out == "" && l.val != nil {
|
||||
l.out = l.val.String()
|
||||
l.val = nil
|
||||
}
|
||||
return l.out
|
||||
}
|
||||
|
||||
// LazyStringer captures a fmt.Stringer implementation resolving the
|
||||
// underlying string *only* when the String() method is called and
|
||||
// caching the result for future use.
|
||||
func LazyStringer(v fmt.Stringer) fmt.Stringer { return &lazyStringer{val: v} }
|
||||
|
||||
type lazyBlockHash struct {
|
||||
block interface{ Hash() tmbytes.HexBytes }
|
||||
out string
|
||||
}
|
||||
|
||||
// LazyBlockHash defers block Hash until the Stringer interface is invoked.
|
||||
// This is particularly useful for avoiding calling Sprintf when debugging is not
|
||||
// active.
|
||||
//
|
||||
// As a result, if you use this type in go routines or defer
|
||||
// statements it's possible to pass an argument to LazyBlockHash that
|
||||
// has one value at the call site and a different value when the
|
||||
// String() is evaluated, which may lead to unexpected outcomes. In
|
||||
// these situations, either be *extremely* careful about the arguments
|
||||
// passed to this function or use fmt.Sprintf.
|
||||
//
|
||||
// The implementation also caches the output of the string form of the
|
||||
// block hash when String() is called, so subsequent calls will
|
||||
// produce the same result.
|
||||
func LazyBlockHash(block interface{ Hash() tmbytes.HexBytes }) fmt.Stringer {
|
||||
return &lazyBlockHash{block: block}
|
||||
}
|
||||
|
||||
func (l *lazyBlockHash) String() string {
|
||||
if l.out == "" && l.block != nil {
|
||||
l.out = l.block.Hash().String()
|
||||
l.block = nil
|
||||
}
|
||||
return l.out
|
||||
}
|
||||
|
||||
// SplitAndTrimEmpty slices s into all subslices separated by sep and returns a
|
||||
// slice of the string s with all leading and trailing Unicode code points
|
||||
// contained in cutset removed. If sep is empty, SplitAndTrim splits after each
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/libs/clist"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
@@ -540,7 +541,7 @@ func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.Respon
|
||||
}
|
||||
|
||||
txmp.logger.Debug("evicting lower-priority transactions",
|
||||
"new_tx", fmt.Sprintf("%X", wtx.tx.Hash()),
|
||||
"new_tx", tmstrings.LazySprintf("%X", wtx.tx.Hash()),
|
||||
"new_priority", priority,
|
||||
)
|
||||
|
||||
@@ -562,7 +563,7 @@ func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.Respon
|
||||
|
||||
txmp.logger.Debug(
|
||||
"evicted valid existing transaction; mempool full",
|
||||
"old_tx", fmt.Sprintf("%X", w.tx.Hash()),
|
||||
"old_tx", tmstrings.LazySprintf("%X", w.tx.Hash()),
|
||||
"old_priority", w.priority,
|
||||
)
|
||||
txmp.removeTxByElement(vic)
|
||||
@@ -588,7 +589,7 @@ func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.Respon
|
||||
txmp.logger.Debug(
|
||||
"inserted new valid transaction",
|
||||
"priority", wtx.Priority(),
|
||||
"tx", fmt.Sprintf("%X", wtx.tx.Hash()),
|
||||
"tx", tmstrings.LazySprintf("%X", wtx.tx.Hash()),
|
||||
"height", txmp.height,
|
||||
"num_txs", txmp.Size(),
|
||||
)
|
||||
|
||||
@@ -132,6 +132,10 @@ func convertTex(in []testTx) types.Txs {
|
||||
}
|
||||
|
||||
func TestTxMempool_TxsAvailable(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -540,6 +544,10 @@ func TestTxMempool_CheckTxSameSender(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTxMempool_ConcurrentTxs(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/libs/clist"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
@@ -330,9 +331,8 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID, m
|
||||
return
|
||||
}
|
||||
|
||||
r.logger.Debug(
|
||||
"gossiped tx to peer",
|
||||
"tx", fmt.Sprintf("%X", memTx.tx.Hash()),
|
||||
r.logger.Debug("gossiped tx to peer",
|
||||
"tx", tmstrings.LazySprintf("%X", memTx.tx.Hash()),
|
||||
"peer", peerID,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ func (c *MConnection) Send(chID ChannelID, msgBytes []byte) bool {
|
||||
// Send message to channel.
|
||||
channel, ok := c.channelsIdx[chID]
|
||||
if !ok {
|
||||
c.logger.Error(fmt.Sprintf("Cannot send bytes, unknown channel %X", chID))
|
||||
c.logger.Error("Cannot send bytes to unknown channel", "channel", chID)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +315,10 @@ func TestMConnectionMultiplePings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMConnectionPingPongs(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// check that we are not leaking any go-routines
|
||||
t.Cleanup(leaktest.CheckTimeout(t, 10*time.Second))
|
||||
|
||||
@@ -558,6 +562,10 @@ func TestMConnectionReadErrorUnknownMsgType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMConnectionTrySend(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
server, client := net.Pipe()
|
||||
t.Cleanup(closeAll(t, client, server))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -606,6 +614,10 @@ func TestConnVectors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMConnectionChannelOverflow(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
chOnErr := make(chan struct{})
|
||||
chOnRcv := make(chan struct{})
|
||||
|
||||
|
||||
@@ -296,6 +296,10 @@ func TestPeerManager_DialNext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPeerManager_DialNext_Retry(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -358,7 +358,8 @@ func (r *Reactor) calculateNextRequestTime(added int) time.Duration {
|
||||
// If the peer store is nearly full, wait the maximum interval.
|
||||
if ratio := r.peerManager.PeerRatio(); ratio >= 0.95 {
|
||||
r.logger.Debug("Peer manager is nearly full",
|
||||
"sleep_period", fullCapacityInterval, "ratio", ratio)
|
||||
"sleep_period", fullCapacityInterval,
|
||||
"ratio", ratio)
|
||||
return fullCapacityInterval
|
||||
}
|
||||
|
||||
|
||||
@@ -208,13 +208,11 @@ func (s *pqScheduler) process(ctx context.Context) {
|
||||
} else {
|
||||
pqEnvTmpChIDStr := strconv.Itoa(int(pqEnvTmp.envelope.ChannelID))
|
||||
s.metrics.PeerQueueDroppedMsgs.With("ch_id", pqEnvTmpChIDStr).Add(1)
|
||||
s.logger.Debug(
|
||||
"dropped envelope",
|
||||
s.logger.Debug("dropped envelope",
|
||||
"ch_id", pqEnvTmpChIDStr,
|
||||
"priority", pqEnvTmp.priority,
|
||||
"msg_size", pqEnvTmp.size,
|
||||
"capacity", s.capacity,
|
||||
)
|
||||
"capacity", s.capacity)
|
||||
|
||||
s.metrics.PeerPendingSendBytes.With("peer_id", string(pqEnvTmp.envelope.To)).Add(float64(-pqEnvTmp.size))
|
||||
|
||||
@@ -238,13 +236,11 @@ func (s *pqScheduler) process(ctx context.Context) {
|
||||
// There is not sufficient capacity to drop lower priority Envelopes,
|
||||
// so we drop the incoming Envelope.
|
||||
s.metrics.PeerQueueDroppedMsgs.With("ch_id", chIDStr).Add(1)
|
||||
s.logger.Debug(
|
||||
"dropped envelope",
|
||||
s.logger.Debug("dropped envelope",
|
||||
"ch_id", chIDStr,
|
||||
"priority", pqEnv.priority,
|
||||
"msg_size", pqEnv.size,
|
||||
"capacity", s.capacity,
|
||||
)
|
||||
"capacity", s.capacity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -461,7 +462,7 @@ func (r *Router) acceptPeers(ctx context.Context, transport Transport) {
|
||||
closeErr := conn.Close()
|
||||
r.logger.Debug("rate limiting incoming peer",
|
||||
"err", err,
|
||||
"ip", incomingIP.String(),
|
||||
"ip", tmstrings.LazyStringer(incomingIP),
|
||||
"close_err", closeErr,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ func echoReactor(ctx context.Context, channel p2p.Channel) {
|
||||
}
|
||||
|
||||
func TestRouter_Network(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -162,6 +166,10 @@ func TestRouter_Channel_Basic(t *testing.T) {
|
||||
|
||||
// Channel tests are hairy to mock, so we use an in-memory network instead.
|
||||
func TestRouter_Channel_SendReceive(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -224,6 +232,10 @@ func TestRouter_Channel_SendReceive(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_Channel_Broadcast(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -255,6 +267,10 @@ func TestRouter_Channel_Broadcast(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_Channel_Wrapper(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -443,6 +459,11 @@ func TestRouter_AcceptPeers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_AcceptPeers_Errors(t *testing.T) {
|
||||
if testing.Short() {
|
||||
// Each subtest takes more than one second due to the time.Sleep call,
|
||||
// so just skip from the parent test in short mode.
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
for _, err := range []error{io.EOF, context.Canceled, context.DeadlineExceeded} {
|
||||
t.Run(err.Error(), func(t *testing.T) {
|
||||
@@ -480,9 +501,7 @@ func TestRouter_AcceptPeers_Errors(t *testing.T) {
|
||||
router.Stop()
|
||||
|
||||
mockTransport.AssertExpectations(t)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +830,10 @@ func TestRouter_EvictPeers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_ChannelCompatability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -59,6 +59,10 @@ func TestMConnTransport_AcceptBeforeListen(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMConnTransport_AcceptMaxAcceptedConnections(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -126,6 +126,10 @@ func TestBlockQueueWithFailures(t *testing.T) {
|
||||
// Test that when all the blocks are retrieved that the queue still holds on to
|
||||
// it's workers and in the event of failure can still fetch the failed block
|
||||
func TestBlockQueueBlocks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
peerID, err := types.NewNodeID("0011223344556677889900112233445566778899")
|
||||
require.NoError(t, err)
|
||||
queue := newBlockQueue(startHeight, stopHeight, 1, stopTime, 2)
|
||||
@@ -176,6 +180,10 @@ loop:
|
||||
}
|
||||
|
||||
func TestBlockQueueAcceptsNoMoreBlocks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
peerID, err := types.NewNodeID("0011223344556677889900112233445566778899")
|
||||
require.NoError(t, err)
|
||||
queue := newBlockQueue(startHeight, stopHeight, 1, stopTime, 1)
|
||||
|
||||
@@ -686,37 +686,31 @@ func (r *Reactor) handleSnapshotMessage(ctx context.Context, envelope *p2p.Envel
|
||||
func (r *Reactor) handleChunkMessage(ctx context.Context, envelope *p2p.Envelope, chunkCh p2p.Channel) error {
|
||||
switch msg := envelope.Message.(type) {
|
||||
case *ssproto.ChunkRequest:
|
||||
r.logger.Debug(
|
||||
"received chunk request",
|
||||
r.logger.Debug("received chunk request",
|
||||
"height", msg.Height,
|
||||
"format", msg.Format,
|
||||
"chunk", msg.Index,
|
||||
"peer", envelope.From,
|
||||
)
|
||||
"peer", envelope.From)
|
||||
resp, err := r.conn.LoadSnapshotChunk(ctx, &abci.RequestLoadSnapshotChunk{
|
||||
Height: msg.Height,
|
||||
Format: msg.Format,
|
||||
Chunk: msg.Index,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(
|
||||
"failed to load chunk",
|
||||
r.logger.Error("failed to load chunk",
|
||||
"height", msg.Height,
|
||||
"format", msg.Format,
|
||||
"chunk", msg.Index,
|
||||
"err", err,
|
||||
"peer", envelope.From,
|
||||
)
|
||||
"peer", envelope.From)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.logger.Debug(
|
||||
"sending chunk",
|
||||
r.logger.Debug("sending chunk",
|
||||
"height", msg.Height,
|
||||
"format", msg.Format,
|
||||
"chunk", msg.Index,
|
||||
"peer", envelope.From,
|
||||
)
|
||||
"peer", envelope.From)
|
||||
if err := chunkCh.Send(ctx, p2p.Envelope{
|
||||
To: envelope.From,
|
||||
Message: &ssproto.ChunkResponse{
|
||||
@@ -739,13 +733,11 @@ func (r *Reactor) handleChunkMessage(ctx context.Context, envelope *p2p.Envelope
|
||||
return nil
|
||||
}
|
||||
|
||||
r.logger.Debug(
|
||||
"received chunk; adding to sync",
|
||||
r.logger.Debug("received chunk; adding to sync",
|
||||
"height", msg.Height,
|
||||
"format", msg.Format,
|
||||
"chunk", msg.Index,
|
||||
"peer", envelope.From,
|
||||
)
|
||||
"peer", envelope.From)
|
||||
_, err := r.syncer.AddChunk(&chunk{
|
||||
Height: msg.Height,
|
||||
Format: msg.Format,
|
||||
@@ -754,14 +746,12 @@ func (r *Reactor) handleChunkMessage(ctx context.Context, envelope *p2p.Envelope
|
||||
Sender: envelope.From,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(
|
||||
"failed to add chunk",
|
||||
r.logger.Error("failed to add chunk",
|
||||
"height", msg.Height,
|
||||
"format", msg.Format,
|
||||
"chunk", msg.Index,
|
||||
"err", err,
|
||||
"peer", envelope.From,
|
||||
)
|
||||
"peer", envelope.From)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,10 @@ func setup(
|
||||
}
|
||||
|
||||
func TestReactor_Sync(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -618,6 +622,10 @@ func TestReactor_StateProviderP2P(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReactor_Backfill(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -626,6 +634,10 @@ func TestReactor_Backfill(t *testing.T) {
|
||||
for _, failureRate := range failureRates {
|
||||
failureRate := failureRate
|
||||
t.Run(fmt.Sprintf("failure rate: %d", failureRate), func(t *testing.T) {
|
||||
if testing.Short() && failureRate > 0 {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -84,11 +84,9 @@ func (s *syncer) AddChunk(chunk *chunk) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
if added {
|
||||
s.logger.Debug("Added chunk to queue", "height", chunk.Height, "format", chunk.Format,
|
||||
"chunk", chunk.Index)
|
||||
s.logger.Debug("Added chunk to queue", "height", chunk.Height, "format", chunk.Format, "chunk", chunk.Index)
|
||||
} else {
|
||||
s.logger.Debug("Ignoring duplicate chunk in queue", "height", chunk.Height, "format", chunk.Format,
|
||||
"chunk", chunk.Index)
|
||||
s.logger.Debug("Ignoring duplicate chunk in queue", "height", chunk.Height, "format", chunk.Format, "chunk", chunk.Index)
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
@@ -137,12 +135,20 @@ func (s *syncer) SyncAny(
|
||||
discoveryTime = minimumDiscoveryTime
|
||||
}
|
||||
|
||||
timer := time.NewTimer(discoveryTime)
|
||||
defer timer.Stop()
|
||||
|
||||
if discoveryTime > 0 {
|
||||
if err := requestSnapshots(); err != nil {
|
||||
return sm.State{}, nil, err
|
||||
}
|
||||
s.logger.Info(fmt.Sprintf("Discovering snapshots for %v", discoveryTime))
|
||||
time.Sleep(discoveryTime)
|
||||
s.logger.Info("discovering snapshots",
|
||||
"interval", discoveryTime)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return sm.State{}, nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
|
||||
// The app may ask us to retry a snapshot restoration, in which case we need to reuse
|
||||
@@ -151,8 +157,11 @@ func (s *syncer) SyncAny(
|
||||
snapshot *snapshot
|
||||
chunks *chunkQueue
|
||||
err error
|
||||
iters int
|
||||
)
|
||||
|
||||
for {
|
||||
iters++
|
||||
// If not nil, we're going to retry restoration of the same snapshot.
|
||||
if snapshot == nil {
|
||||
snapshot = s.snapshots.Best()
|
||||
@@ -162,9 +171,16 @@ func (s *syncer) SyncAny(
|
||||
if discoveryTime == 0 {
|
||||
return sm.State{}, nil, errNoSnapshots
|
||||
}
|
||||
s.logger.Info(fmt.Sprintf("Discovering snapshots for %v", discoveryTime))
|
||||
time.Sleep(discoveryTime)
|
||||
continue
|
||||
s.logger.Info("discovering snapshots",
|
||||
"iterations", iters,
|
||||
"interval", discoveryTime)
|
||||
timer.Reset(discoveryTime)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return sm.State{}, nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
if chunks == nil {
|
||||
chunks, err = newChunkQueue(snapshot, s.tempDir)
|
||||
@@ -494,13 +510,11 @@ func (s *syncer) requestChunk(ctx context.Context, snapshot *snapshot, chunk uin
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Debug(
|
||||
"Requesting snapshot chunk",
|
||||
s.logger.Debug("Requesting snapshot chunk",
|
||||
"height", snapshot.Height,
|
||||
"format", snapshot.Format,
|
||||
"chunk", chunk,
|
||||
"peer", peer,
|
||||
)
|
||||
"peer", peer)
|
||||
|
||||
msg := p2p.Envelope{
|
||||
To: peer,
|
||||
|
||||
@@ -22,6 +22,10 @@ import (
|
||||
)
|
||||
|
||||
func TestSyncer_SyncAny(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -55,9 +55,7 @@ func NewDefaultLogger(format, level string) (Logger, error) {
|
||||
// make the writer thread-safe
|
||||
logWriter = newSyncWriter(logWriter)
|
||||
|
||||
return &defaultLogger{
|
||||
Logger: zerolog.New(logWriter).Level(logLevel).With().Timestamp().Logger(),
|
||||
}, nil
|
||||
return &defaultLogger{Logger: zerolog.New(logWriter).Level(logLevel).With().Timestamp().Logger()}, nil
|
||||
}
|
||||
|
||||
func (l defaultLogger) Info(msg string, keyVals ...interface{}) {
|
||||
@@ -73,9 +71,7 @@ func (l defaultLogger) Debug(msg string, keyVals ...interface{}) {
|
||||
}
|
||||
|
||||
func (l defaultLogger) With(keyVals ...interface{}) Logger {
|
||||
return &defaultLogger{
|
||||
Logger: l.Logger.With().Fields(keyVals).Logger(),
|
||||
}
|
||||
return &defaultLogger{Logger: l.Logger.With().Fields(keyVals).Logger()}
|
||||
}
|
||||
|
||||
// OverrideWithNewLogger replaces an existing logger's internal with
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
"github.com/tendermint/tendermint/light/provider"
|
||||
@@ -475,7 +476,8 @@ func (c *Client) VerifyHeader(ctx context.Context, newHeader *types.Header, now
|
||||
return fmt.Errorf("existing trusted header %X does not match newHeader %X", l.Hash(), newHeader.Hash())
|
||||
}
|
||||
c.logger.Debug("header has already been verified",
|
||||
"height", newHeader.Height, "hash", newHeader.Hash())
|
||||
"height", newHeader.Height,
|
||||
"hash", tmstrings.LazyBlockHash(newHeader))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -576,7 +578,7 @@ func (c *Client) verifySequential(
|
||||
// 2) Verify them
|
||||
c.logger.Debug("verify adjacent newLightBlock against verifiedBlock",
|
||||
"trustedHeight", verifiedBlock.Height,
|
||||
"trustedHash", verifiedBlock.Hash(),
|
||||
"trustedHash", tmstrings.LazyBlockHash(verifiedBlock),
|
||||
"newHeight", interimBlock.Height,
|
||||
"newHash", interimBlock.Hash())
|
||||
|
||||
@@ -663,9 +665,9 @@ func (c *Client) verifySkipping(
|
||||
for {
|
||||
c.logger.Debug("verify non-adjacent newHeader against verifiedBlock",
|
||||
"trustedHeight", verifiedBlock.Height,
|
||||
"trustedHash", verifiedBlock.Hash(),
|
||||
"trustedHash", tmstrings.LazyBlockHash(verifiedBlock),
|
||||
"newHeight", blockCache[depth].Height,
|
||||
"newHash", blockCache[depth].Hash())
|
||||
"newHash", tmstrings.LazyBlockHash(blockCache[depth]))
|
||||
|
||||
// Verify the untrusted header. This function is equivalent to
|
||||
// ValidAndVerified in the spec
|
||||
@@ -897,9 +899,9 @@ func (c *Client) backwards(
|
||||
interimHeader = interimBlock.Header
|
||||
c.logger.Debug("verify newHeader against verifiedHeader",
|
||||
"trustedHeight", verifiedHeader.Height,
|
||||
"trustedHash", verifiedHeader.Hash(),
|
||||
"trustedHash", tmstrings.LazyBlockHash(verifiedHeader),
|
||||
"newHeight", interimHeader.Height,
|
||||
"newHash", interimHeader.Hash())
|
||||
"newHash", tmstrings.LazyBlockHash(interimHeader))
|
||||
if err := VerifyBackwards(interimHeader, verifiedHeader); err != nil {
|
||||
// verification has failed
|
||||
c.logger.Info("backwards verification failed, replacing primary...", "err", err, "primary", c.primary)
|
||||
|
||||
@@ -39,8 +39,10 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig
|
||||
lastVerifiedHeader = primaryTrace[len(primaryTrace)-1].SignedHeader
|
||||
witnessesToRemove = make([]int, 0)
|
||||
)
|
||||
c.logger.Debug("running detector against trace", "finalizeBlockHeight", lastVerifiedHeader.Height,
|
||||
"finalizeBlockHash", lastVerifiedHeader.Hash, "length", len(primaryTrace))
|
||||
c.logger.Debug("running detector against trace",
|
||||
"finalizeBlockHeight", lastVerifiedHeader.Height,
|
||||
"finalizeBlockHash", lastVerifiedHeader.Hash,
|
||||
"length", len(primaryTrace))
|
||||
|
||||
// launch one goroutine per witness to retrieve the light block of the target height
|
||||
// and compare it with the header from the primary
|
||||
|
||||
@@ -235,6 +235,10 @@ func TestLightClientAttackEvidence_Equivocation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// primary performs a lunatic attack but changes the time of the header to
|
||||
// something in the future relative to the blockchain
|
||||
var (
|
||||
|
||||
@@ -17,6 +17,10 @@ import (
|
||||
|
||||
// Manually getting light blocks and verifying them.
|
||||
func TestExampleClient(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
conf, err := rpctest.CreateConfig(t, "ExampleClient_VerifyLightBlockAtHeight")
|
||||
|
||||
@@ -23,6 +23,10 @@ import (
|
||||
|
||||
// Automatically getting new headers and verifying them.
|
||||
func TestClientIntegration_Update(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -84,6 +88,10 @@ func TestClientIntegration_Update(t *testing.T) {
|
||||
|
||||
// Manually getting light blocks and verifying them.
|
||||
func TestClientIntegration_VerifyLightBlockAtHeight(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -162,6 +170,10 @@ func waitForBlock(ctx context.Context, p provider.Provider, height int64) (*type
|
||||
}
|
||||
|
||||
func TestClientStatusRPC(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
conf, err := rpctest.CreateConfig(t, t.Name())
|
||||
|
||||
@@ -33,6 +33,10 @@ func TestNewProvider(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
cfg, err := rpctest.CreateConfig(t, t.Name())
|
||||
|
||||
@@ -106,6 +106,10 @@ func getTestNode(ctx context.Context, t *testing.T, conf *config.Config, logger
|
||||
}
|
||||
|
||||
func TestNodeDelayedStart(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
cfg, err := config.ResetTestRoot(t.TempDir(), "node_delayed_start_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -195,6 +199,10 @@ func TestNodeSetPrivValTCP(t *testing.T) {
|
||||
|
||||
// address without a protocol must result in error
|
||||
func TestPrivValidatorListenAddrNoProtocol(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -441,6 +449,10 @@ func TestMaxTxsProposalBlockSize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMaxProposalBlockSize(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ import (
|
||||
)
|
||||
|
||||
func TestHTTPSimple(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -68,6 +72,10 @@ func TestHTTPSimple(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHTTPBatching(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
)
|
||||
|
||||
func TestWaitForHeight(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ func TestClientOperations(t *testing.T) {
|
||||
})
|
||||
t.Run("Batching", func(t *testing.T) {
|
||||
t.Run("JSONRPCCalls", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
logger := log.NewTestingLogger(t)
|
||||
c := getHTTPClient(t, logger, conf)
|
||||
testBatchedJSONRPCCalls(ctx, t, c)
|
||||
@@ -171,6 +175,10 @@ func TestClientOperations(t *testing.T) {
|
||||
require.Zero(t, batch.Clear(), "clearing an empty batch of JSON RPC requests should result in a 0 result")
|
||||
})
|
||||
t.Run("ConcurrentJSONRPC", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
logger := log.NewTestingLogger(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -291,6 +299,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
"first: %+v, doc: %s", first, string(doc))
|
||||
})
|
||||
t.Run("ABCIQuery", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// write something
|
||||
k, v, tx := MakeTxKV()
|
||||
status, err := c.Status(ctx)
|
||||
@@ -309,6 +321,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("AppCalls", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// get an offset of height to avoid racing and guessing
|
||||
s, err := c.Status(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -409,6 +425,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
// XXX Test proof
|
||||
})
|
||||
t.Run("BlockchainInfo", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -439,6 +459,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "can't be greater than max")
|
||||
})
|
||||
t.Run("BroadcastTxCommit", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
_, _, tx := MakeTxKV()
|
||||
bres, err := c.BroadcastTxCommit(ctx, tx)
|
||||
require.NoError(t, err, "%d: %+v", i, err)
|
||||
@@ -481,6 +505,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
// TODO: more checks...
|
||||
})
|
||||
t.Run("Block", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
const subscriber = "TestBlockEvents"
|
||||
|
||||
eventCh, err := c.Subscribe(ctx, subscriber, types.QueryForEvent(types.EventNewBlockValue).String())
|
||||
@@ -515,6 +543,10 @@ func TestClientMethodCalls(t *testing.T) {
|
||||
})
|
||||
t.Run("Evidence", func(t *testing.T) {
|
||||
t.Run("BroadcastDuplicateVote", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -722,6 +754,10 @@ func TestClientMethodCallsAdvanced(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("TxSearchWithTimeout", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
logger := log.NewTestingLogger(t)
|
||||
|
||||
timeoutClient := getHTTPClientWithTimeout(t, logger, conf, 10*time.Second)
|
||||
|
||||
@@ -65,6 +65,10 @@ func (h *myTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func TestWSClientReconnectsAfterReadFailure(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
|
||||
// start server
|
||||
@@ -97,6 +101,10 @@ func TestWSClientReconnectsAfterReadFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWSClientReconnectsAfterWriteFailure(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
|
||||
// start server
|
||||
@@ -127,6 +135,10 @@ func TestWSClientReconnectsAfterWriteFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWSClientReconnectFailure(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
t.Cleanup(leaktest.Check(t))
|
||||
|
||||
// start server
|
||||
|
||||
@@ -340,6 +340,10 @@ func TestRPC(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("WSClientPingPong", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// TestWSClientPingPong checks that a client & server exchange pings
|
||||
// & pongs so connection stays alive.
|
||||
t.Cleanup(leaktest.CheckTimeout(t, 4*time.Second))
|
||||
|
||||
@@ -58,7 +58,7 @@ func DefaultConfig() *Config {
|
||||
// Serve creates a http.Server and calls Serve with the given listener. It
|
||||
// wraps handler to recover panics and limit the request body size.
|
||||
func Serve(ctx context.Context, listener net.Listener, handler http.Handler, logger log.Logger, config *Config) error {
|
||||
logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr()))
|
||||
logger.Info("Starting RPC HTTP server on", "addr", listener.Addr())
|
||||
h := recoverAndLogHandler(MaxBytesHandler(handler, config.MaxBodyBytes), logger)
|
||||
s := &http.Server{
|
||||
Handler: h,
|
||||
|
||||
14
spec/p2p/v0.35/README.md
Normal file
14
spec/p2p/v0.35/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Peer-to-peer communication substrate - WIP
|
||||
|
||||
This document details the operation of the [`p2p`][p2p-package] package of
|
||||
Tendermint, refactored in the `v0.35` release.
|
||||
|
||||
**This is a work in progress** ([#8935][issue]). The following files represent the current (unfinished) state of this documentation. It has been decided not to finish the documents at this point in time, but to publish them here in the current form for future reference.
|
||||
|
||||
- [Peer manager](./peer_manager.md): determines when a node should connect to a
|
||||
new peer, and which peer is preferred for establishing connections.
|
||||
- [Router](./router.md): implements the actions instructed by the peer manager,
|
||||
and route messages between the local reactors and the remote peers.
|
||||
|
||||
[issue]: https://github.com/tendermint/tendermint/issues/8935
|
||||
[p2p-package]: https://github.com/tendermint/tendermint/tree/v0.35.x/internal/p2p
|
||||
473
spec/p2p/v0.35/peer_manager.md
Normal file
473
spec/p2p/v0.35/peer_manager.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# Peer manager - WIP
|
||||
|
||||
The peer manager is the component of the *new* p2p layer that implements
|
||||
the connection policy for the node, based on the
|
||||
configuration provided by the operators, the current state of the connections
|
||||
reported by the [`Router`](./router.md), and the set of known candidate peers.
|
||||
|
||||
This document uses *candidate peer* to refer to the information about a node in
|
||||
the network, namely its unique identifier and one or more network addresses.
|
||||
This information can be manually configured by the node operator (e.g., via
|
||||
`PersistentPeers` parameter) or can be obtained via the Peer-Exchange Protocol
|
||||
(PEX), which feeds the peer manager with discovered peers' information.
|
||||
|
||||
## Connection policy
|
||||
|
||||
The connection policy defines:
|
||||
|
||||
1. When the node should establish new connections to peers, and
|
||||
1. The next peer to which the router should try to establish a connection.
|
||||
|
||||
The first definition is made based on the concept of [connection slots](#connection-slots).
|
||||
In short, the peer manager will try to fill every connection slot with a peer.
|
||||
If all the connection slots are full but there is the possibility to connect to
|
||||
a peer that is higher-[ranked](#peer-ranking) than one of the connecting peers,
|
||||
the peer manager may attempt to [upgrade a connection slot](#slot-upgrades).
|
||||
Details of these operations are provided in the following.
|
||||
|
||||
### Connection slots
|
||||
|
||||
The number of connection slots is defined by the `MaxConnected` parameter.
|
||||
|
||||
While there are available connection slots, the peer manager will provide
|
||||
candidate peers to the router, which will try to establish
|
||||
new connections to them.
|
||||
When the peer manager provides a candidate peer to
|
||||
the router, a connection slot becomes _virtually_ occupied by the peer, as the
|
||||
router should be dialing it.
|
||||
|
||||
When the router establishes a connection to a peer, either
|
||||
because it [accepted a connection](#accepted-transition) from a peer,
|
||||
or because it [successfully dialed](#dialed-transition) a candidate peer,
|
||||
the peer manager should find a slot for this connection.
|
||||
|
||||
If there is an available connection slot, and this is the first connection
|
||||
established with that peer, the slot is filled by the new connection and
|
||||
the peer becomes a [connected peer](#connected-peer).
|
||||
The peer manager does not allow two slots to be filled with connections to the
|
||||
same peer.
|
||||
|
||||
If all `MaxConnected` connection slots are full, the node should _a priori_
|
||||
reject the connection established with or accepted from the peer.
|
||||
However, it is possible that the new connection is with a peer whose score is
|
||||
better than the score of a peer occupying one of the connection slots.
|
||||
In this case, the peer manager will try to [upgrade the slot](#slot-upgrades)
|
||||
to make room to the new connection, by evicting the peer currently occupying
|
||||
this slot.
|
||||
|
||||
> Although not advisable, the `MaxConnected` parameter can be set to `0`, which
|
||||
> means no limit.
|
||||
>
|
||||
> In this case, the node will accept all connections established by peers, and
|
||||
> will try to establish connections (dial) to every candidate peer it knows.
|
||||
|
||||
### Outgoing connections
|
||||
|
||||
The peer manager distinguishes *incoming* from *outgoing* connections.
|
||||
A connection is *incoming* when the router has [accepted](#accepted-transition)
|
||||
it from a peer.
|
||||
A connection is *outgoing* when the router has successfully
|
||||
[dialed](#dialed-transition) a peer.
|
||||
|
||||
If the `MaxOutgoingConnections` parameter is set (i.e., it is greater than zero), it
|
||||
defines the maximum number of *outgoing* connections the node should maintain.
|
||||
More precisely, it determines that the node should not attempt to dial new
|
||||
peers when the router already has established outgoing connections to
|
||||
`MaxOutgoingConnections` peers.
|
||||
This parameter cannot be set to a value larger than `MaxConnected`.
|
||||
|
||||
> The previous version of the `p2p` explicitly distinguished incoming and
|
||||
> outgoing peers. Configuring the `MaxOutgoingConnections` parameters should
|
||||
> therefore make the connection policy similar to the one adopted in the
|
||||
> previous version. (TODO: check)
|
||||
|
||||
### Slot upgrades
|
||||
|
||||
The rationale behind this concept is that the node may try to establish or
|
||||
accept connections even when all the connection slots are full, provided that
|
||||
the peer in the other side of the new connection is better-ranked than a peer
|
||||
that is occupying a connection slot.
|
||||
A slot can therefore be upgraded, meaning that the lower-ranked peer
|
||||
occupying the slot will be replaced by a higher-ranked peer.
|
||||
|
||||
The upgrading of connection slots is determined by the `MaxConnectedUpgrade`
|
||||
parameter, which defines the number of connections that the peer manager can
|
||||
use for upgrading connection slots.
|
||||
|
||||
If `MaxConnectedUpgrade` is set to zero, the upgrading of connection slots is
|
||||
disabled.
|
||||
This means, in particular, that `MaxConnected` is the hard limit of peers that
|
||||
can be in the [connected state](#connected-peer).
|
||||
|
||||
If `MaxConnectedUpgrade` is larger than zero, the upgrading of connection slots
|
||||
is enabled.
|
||||
As a result, the hard limit for the number of peers that can be in the
|
||||
[connected state](#connected-peer) becomes `MaxConnected + MaxConnectedUpgrade`.
|
||||
Some of these peers, however, will not remain in this state as they should be
|
||||
[evicted](#evictnext-transition) by the router.
|
||||
|
||||
### Peer ranking
|
||||
|
||||
The peer manager should rank peers based on user-provided parameters and on the
|
||||
current state of the peer.
|
||||
|
||||
The ranking is established by ordering all known peers by its score.
|
||||
This mechanism is currently very basic.
|
||||
|
||||
> The code contains a number of potential replacements for this ranking
|
||||
> mechanism. Therefore, improving this mechanism is a work in progress.
|
||||
|
||||
Peers configured as `PersistentPeers` have _always_ `PeerScorePersistent`,
|
||||
which is the maximum allowed peer score.
|
||||
|
||||
The remaining peers have a `MutableScore`, initialized to `0` when the peer is
|
||||
added to the peer manager.
|
||||
When the peer is reported as a `PeerStatusGood`, its score is incremented.
|
||||
When the peer is reported as a `PeerStatusBad`, its score is decremented.
|
||||
|
||||
> The mechanisms based on the "reputation" of the peer according to reactors,
|
||||
> however, appears not to be fully implemented.
|
||||
> A peer is never `PeerStatusGood`, and is only reported as `PeerStatusBad` a
|
||||
> reactor interacting with the peer reports an error to the router, and the
|
||||
> error is not "fatal".
|
||||
> If the error is fatal, the peer is reported as [errored](#errored-transition).
|
||||
|
||||
This score can also be _temporarily_ decremented due to connection errors.
|
||||
When the router fails to dial to a peer, it increments the peer's
|
||||
`DialFailures` counter.
|
||||
This counter is reset when the router successfully dials the peer, establishing
|
||||
a connection to it.
|
||||
During this period, between dial failures and succeeding to dial the peer, the
|
||||
peer score is decremented by the `DialFailures` counter.
|
||||
|
||||
> `DialFailures` actually refers to a peer address. A peer may have multiple
|
||||
> addresses, and all associated counters are considered for decrementing the
|
||||
> peer's score. Also, all counters are reset when the router succeeds dialing
|
||||
> the peer.
|
||||
|
||||
## Peer life cycle
|
||||
|
||||
For implementing the connection policy, the peer manager keeps track of the
|
||||
state of peers and manages their life-cycle.
|
||||
The life cycle of a peer is summarized in the picture below.
|
||||
The circles represent _states_ of a peer and the rectangles represent
|
||||
_transitions_.
|
||||
All transitions are performed by the `Router`, by invoking methods of the peer
|
||||
manager with corresponding names.
|
||||
Normal transitions are represented by green arrows, while red arrows represent
|
||||
alternative transitions taken in case of errors.
|
||||
|
||||
<img src="pics/p2p-v0.35-peermanager.png" alt="peer life cycle" title="" width="600px" name="" align="center"/>
|
||||
|
||||
### Candidate peer
|
||||
|
||||
The initial state of a peer in the peer manager.
|
||||
|
||||
A `Candidate` peer may become an actual peer, to which the node is connected.
|
||||
We do not use candidate to refer to a peer to which we are connected, nor to
|
||||
a peer we are attempting to connect.
|
||||
|
||||
Candidate peers from which the router recently disconnected or failed to dial
|
||||
are, during a certain period, not eligible for establishing connections.
|
||||
This scenario is represented by the `Frozen Candidate` state.
|
||||
|
||||
### DialNext transition
|
||||
|
||||
This state transition produces candidate peers the node should dial to, which
|
||||
are consumed by the [dialing routine](./router.md#dialing-peers) of the router.
|
||||
|
||||
The transition is performed when the [connection policy](#connection-policy)
|
||||
determines that the node should try to establish a connection with a peer.
|
||||
|
||||
The algorithm controlling this state transition can be synthesized as follows:
|
||||
|
||||
1. Wait until there are peers in `Candidate` state
|
||||
1. Select the best-ranked `peer` in `Candidate` state
|
||||
1. If `|Candidate + Dialing| < MaxConnected`, returns the selected `peer`
|
||||
1. Else if `|Candidate + Dialing| < MaxConnected + MaxConnectedUpgrade`, try to
|
||||
find a connection `slot` to upgrade to give room to the selected `peer`
|
||||
1. If a connection `slot` to upgrade is found, set the peer in the slot to
|
||||
the `Upgrading` sub-state and returns the selected `peer`
|
||||
|
||||
The peer manager selects the [best-ranked](#peer-ranking) peer which is in the
|
||||
[`Candidate`](#candidate-peer) state and provides it to the router.
|
||||
As the router is supposed to dial the peer, the peer manager sets the peer to
|
||||
the [dialing](#dialing-peer) state.
|
||||
|
||||
Dialing a candidate peer may have become possible because the peer manager
|
||||
has found a [connection slot to upgrade](#slot-upgrades) to given room to the
|
||||
selected candidate peer.
|
||||
If this is the case, the peer occupying this connection slot is set to the
|
||||
[upgrading state](#upgrading-peer), and will be evicted once the
|
||||
connection to the candidate peer is successfully established.
|
||||
|
||||
### Dialing peer
|
||||
|
||||
A peer that has been returned to the router as the next peer to dial.
|
||||
The router should be attempting to connect to this peer.
|
||||
|
||||
A peer in `Dialing` state is not considered as a candidate peer.
|
||||
|
||||
### Dialed transition
|
||||
|
||||
This transition is performed when the node establishes an outgoing connection
|
||||
with a peer.
|
||||
This means that the peer manager has provided this peer to the router as the
|
||||
[next peer to dial](#dialnext-transition), and the router has dialed and
|
||||
successfully established a connection with the peer.
|
||||
The peer is thus expected to be in the `Dialing` state.
|
||||
|
||||
It may occur, however, that when this transition is invoked the peer is already
|
||||
in the `Connected` state.
|
||||
The most likely reason is that the router, while dialing this peer, has also
|
||||
accepted an incoming connection from the same peer.
|
||||
In this case, the transition fails, indicating to the router that is should
|
||||
close the newly established connection.
|
||||
|
||||
It may also occur that the node is already connected to `MaxConnected` peers,
|
||||
which means that all connection slots are full.
|
||||
In this case, the peer manager tries to find a connection slot that can be
|
||||
[upgraded](#slot-upgrades) to give room for the new established connection.
|
||||
If no suitable connection slot is found, the transitions fails.
|
||||
This logic considered in this step is synthesized by the following algorithm:
|
||||
|
||||
1. If `|Connected| < MaxConnected`, the transition succeeds
|
||||
1. The established connection occupies one of the available connection slots
|
||||
1. If a connected peer was put in the `Upgrading` sub-state to give room to this peer
|
||||
1. Let `slot` be the connection slot occupied by this peer
|
||||
1. If `|Connected| < MaxConnected + MaxConnectedUpgrade`
|
||||
1. Let `slot` be a connection slot that can be upgraded to give room to
|
||||
the established connection, if any
|
||||
1. If `slot` is set to a valid connection slot, the transition succeeds
|
||||
1. Set the peer occupying `slot` to the `Evict` sub-state
|
||||
1. The established connection occupies one of the connection slots reserved for upgrades
|
||||
1. Else the transition fails and the connection is refused
|
||||
|
||||
Notice that, in order to dial this peer, the peer manager may have put another
|
||||
lower-ranked peer in the [upgrading sub-state](#upgrading-peer) to give room
|
||||
to this connection.
|
||||
In this case, as illustrated above, the slot for the established connection was
|
||||
*reserved*, and this transition will not fail.
|
||||
|
||||
If the transition succeeds, the peer is set to the
|
||||
[`Connected`](#connected-peer) state as an _outgoing_ peer.
|
||||
The peer's `LastConnected` and the dialed address' `LastDialSuccess` times are
|
||||
set, and dialed address' `DialFailures` counter is reset.
|
||||
|
||||
> If the peer is `Inactive`, it is set as active.
|
||||
> This action has no effect apart from producing metrics.
|
||||
|
||||
If a connection slot was upgraded to give room for the established connection, the
|
||||
peer on that slot transitions to the [evict sub-state](#evict-peer).
|
||||
|
||||
#### Errors
|
||||
|
||||
The transition fails if:
|
||||
|
||||
- the node dialed itself
|
||||
- the peer is already in the `Connected` state
|
||||
- the node is connected to `MaxConnected` peers, and no slot is suitable for upgrading
|
||||
|
||||
Errors are also returned if:
|
||||
|
||||
- the dialed peer was pruned from the peer store (because it had more than `MaxPeers` stored)
|
||||
- the updated peer information is invalid
|
||||
- there is an error when saving the peer state to the peer store
|
||||
|
||||
### DialFailed transition
|
||||
|
||||
This transition informs of a failure when establishing an outgoing connection to
|
||||
a peer.
|
||||
|
||||
The dialed address's `LastDialFailure` time is set, and its `DialFailures`
|
||||
counter is increased.
|
||||
This information is used to compute the [retry delay](#retry-delay) for the
|
||||
dialed address.
|
||||
|
||||
The peer manager then spawns a routine that after the computed retry delay
|
||||
notifies the next peer to dial routine about the availability of this peer.
|
||||
Until then, the peer is the `Frozen Candidate` state.
|
||||
|
||||
#### Retry delay
|
||||
|
||||
The retry delay is the minimum time, from the latest failed dialing attempt, we
|
||||
should wait until dialing a peer address again.
|
||||
|
||||
The default delay is defined by `MinRetryTime` parameter.
|
||||
If it is set to zero, we *never* retry dialing a peer address.
|
||||
|
||||
Upon each failed dial attempt, we increase the delay by `MinRetryTime`, plus an
|
||||
optional random jitter of up to `RetryTimeJitter`.
|
||||
|
||||
The retry delay should not be longer than the `MaxRetryTime` parameter,
|
||||
or `MaxRetryTimePersistent` parameter in the case of persistent peers.
|
||||
|
||||
#### Errors
|
||||
|
||||
Errors are also returned if:
|
||||
|
||||
- the updated peer information is invalid
|
||||
- there is an error when saving the peer state to the peer store
|
||||
|
||||
### Accepted transition
|
||||
|
||||
This transition is performed when the node establishes an *incoming* connection
|
||||
with a peer.
|
||||
This means that the router has received a connection attempt from this peer and
|
||||
successfully established a connection with it.
|
||||
|
||||
It may occur, however, that when this transition is invoked the peer is already
|
||||
in the [`Connected`](#connected-peer) state.
|
||||
The most likely reason is that the router was simultaneously dialing the same
|
||||
peer, and has successfully [established a connection](#dialed-transition) with
|
||||
it.
|
||||
In this case, the transition fails, indicating to the router that it should
|
||||
close the accepted connection.
|
||||
|
||||
It may also occur that the node is already connected to `MaxConnected` peers,
|
||||
which means that all connection slots are full.
|
||||
In this case, the peer manager tries to find a connection slot that can be
|
||||
[upgraded](#slot-upgrades) to give room for the accepted connection.
|
||||
If no suitable connection slot is found, the transitions fails.
|
||||
This logic considered in this step is synthesized by the following algorithm:
|
||||
|
||||
1. If `|Connected| < MaxConnected`, the transition succeeds
|
||||
1. The established connection occupies one of the available connection slots
|
||||
1. Let `slot` be a connection slot that can be upgraded to give room to the
|
||||
established connection, if any
|
||||
1. If `|Connected| < MaxConnected + MaxConnectedUpgrade` and `slot` is set to a
|
||||
valid connection slot, the transition succeeds
|
||||
1. Set the peer occupying `slot` to the `Evict` sub-state
|
||||
1. The established connection occupies one of the connection slots reserved for upgrades
|
||||
1. Else the transition fails and the connection is refused
|
||||
|
||||
If the transition succeeds, the peer is set to the
|
||||
[`Connected`](#connected-peer) state as an *incoming* peer.
|
||||
|
||||
The accepted peer might not be known by the peer manager.
|
||||
In this case the peer is registered in the peer store, without any associated
|
||||
address (as the connection remote address usually _is not_ the peer's listen address).
|
||||
The peer's `LastConnected` time is set and the `DialFailures` counter is reset
|
||||
for all addresses associated to the peer.
|
||||
|
||||
> If the peer is `Inactive`, it is set as active.
|
||||
> This action has not effect apart from producing metrics.
|
||||
|
||||
If a connection slot was upgraded to give room for the accepted connection, the
|
||||
peer on that slot transitions to the [evict sub-state](#evict-peer).
|
||||
|
||||
#### Errors
|
||||
|
||||
The transition fails if:
|
||||
|
||||
- the node accepted itself
|
||||
- the peer is already in the `Connected` state
|
||||
- the node is connected to `MaxConnected` peers, and no slot is suitable for upgrading
|
||||
|
||||
Errors are also returned if:
|
||||
|
||||
- the updated peer information is invalid
|
||||
- there is an error when saving the peer state to the peer store
|
||||
|
||||
### Connected peer
|
||||
|
||||
A peer to which the node is connected.
|
||||
A peer in this state is not considered a candidate peer.
|
||||
|
||||
The peer manager distinguishes *incoming* from *outgoing* connections.
|
||||
Incoming connections are established through the [`Accepted`](#accepted-transition) transition.
|
||||
Outgoing connections are established through the [`Dialed`](#dialed-transition) transition.
|
||||
|
||||
### Ready transition
|
||||
|
||||
By invoking this transition, the router notifies the peer manager that it is
|
||||
ready to exchange messages with a peer.
|
||||
|
||||
The router invokes this transition just after successfully performing the
|
||||
[`Dialed`](#dialed-transition) or [`Accepted`](#accepted-transition) transitions,
|
||||
providing to the peer manager a list of channels supported by the peer.
|
||||
This information is broadcast to all reactors in a `PeerUpdate` message that
|
||||
informs the new state (up) of the peer.
|
||||
|
||||
This transition is not represented in the picture because it does not change
|
||||
the state of the peer, which should be in the [`Connected`](#connected-peer) state.
|
||||
|
||||
### Disconnected transition
|
||||
|
||||
This transition is performed when the node disconnects from a peer.
|
||||
It is invoked by the router when an error is returned by the routines used to
|
||||
exchange messages with the peer.
|
||||
|
||||
The peer is expected to be in the [`Connected`](#connected-peer) state.
|
||||
If the [`Ready`](#ready-transition) transition has been performed, the peer manager broadcasts a
|
||||
`PeerUpdate` to all reactors notifying the new status (down) of this peer.
|
||||
|
||||
If the peer is still present in the peer store, its `LastDisconnected` time is
|
||||
set and the peer manager spawns a routine that after `DisconnectCooldownPeriod`
|
||||
notifies the next peer to dial routine about the availability of this peer.
|
||||
Until then, the peer is the `Frozen Candidate` state.
|
||||
|
||||
### Errored transition
|
||||
|
||||
This transition is performed when a reactor interacting with the peer reports
|
||||
an error to the router.
|
||||
|
||||
The peer is expected to be in the [`Connected`](#connected-peer) state.
|
||||
If so, the peer transitions to the [`Evict`](#evict-peer) sub-state, which
|
||||
should lead the router to disconnect from the peer, and the next peer to evict
|
||||
routine is notified.
|
||||
|
||||
### Upgrading peer
|
||||
|
||||
A [connected peer](#connected-peer) which should be evicted to give room to a
|
||||
higher-ranked peer the router is dialing to.
|
||||
|
||||
The `Upgrading` sub-state is part of the procedure to [upgrade connection slots](#slot-upgrades).
|
||||
When a connection with the higher-ranked peer that should take the connection
|
||||
slot from this peer is [established](#dialed-transition), the
|
||||
[eviction](#evict-peer) of this peer is scheduled.
|
||||
|
||||
### Evict peer
|
||||
|
||||
A peer whose eviction was scheduled, for either of the following reasons:
|
||||
|
||||
1. to give room to a higher-ranked peer the router is connected to, as part of
|
||||
the procedure to [upgrade connection slots](#slot-upgrades),
|
||||
2. or because this peer was reported as [errored](#errored-transition) by a
|
||||
reactor interacting with this peer.
|
||||
|
||||
This peer is a [connected peer](#connected-peer).
|
||||
`Evict` is the first sub-state of the procedure that should lead the node to
|
||||
[disconnect](#disconnected-transition) from a peer.
|
||||
|
||||
### EvictNext transition
|
||||
|
||||
This transition returns a peer to the router to evict.
|
||||
|
||||
The state transition is performed whenever the peer manager has scheduled the
|
||||
eviction of a peer, i.e., whenever there is a peer on `Evict` sub-state.
|
||||
The peer to evict must be a peer in the `Connected` state.
|
||||
|
||||
The peer to evict is randomly picked from the possible multiple peers with
|
||||
eviction scheduled.
|
||||
|
||||
> This transition is invoked when the next to evict routine is notified by
|
||||
> another routine.
|
||||
> In some cases, the transition is processed when no peer should be evicted. In
|
||||
> this case, if the connections slots are not full, or there are enough peers
|
||||
> in the `Evicting` state so to respect the `MaxConnected` parameter, the
|
||||
> transition is not taken.
|
||||
> Otherwise, the peer with the lowest rank is evicted. This should not occur,
|
||||
> from comments in the code, but this is something to check.
|
||||
|
||||
### Evicting peer
|
||||
|
||||
A peer whose eviction is in progress.
|
||||
A peer transitions to this sub-state when it is returned to the router by the
|
||||
[next peer to evict](#evictnext-transition) transition.
|
||||
|
||||
This peer is still a [connected peer](#connected-peer).
|
||||
`Evicting` is the second and last sub-state of the procedure for
|
||||
[disconnecting](#disconnected-transition) from a peer.
|
||||
|
||||
[peermanager.go]: https://github.com/tendermint/tendermint/blob/v0.35.x/internal/p2p/peermanager.go
|
||||
BIN
spec/p2p/v0.35/pics/p2p-v0.35-peermanager.png
Normal file
BIN
spec/p2p/v0.35/pics/p2p-v0.35-peermanager.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
128
spec/p2p/v0.35/router.md
Normal file
128
spec/p2p/v0.35/router.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Router - WIP
|
||||
|
||||
The router is the component of the *new* p2p layer
|
||||
responsible for establishing connection with peers,
|
||||
and for routing messages from reactors to peers and vice-versa.
|
||||
|
||||
## Dialing peers
|
||||
|
||||
The router maintains a persistent routine `dialPeers()` consuming
|
||||
[candidate peers to dial](./peer_manager.md#dialnext-transition)
|
||||
produced by the peer manager.
|
||||
|
||||
The consumed candidate peers (addresses) are provided for dialing routines,
|
||||
retrieved from a pool with `numConcurrentDials()` threads.
|
||||
The default number of threads in the pool is set to 32 times the number of
|
||||
available CPUs.
|
||||
|
||||
> The 32 factor was introduced in [#8827](https://github.com/tendermint/tendermint/pull/8827),
|
||||
> with the goal of speeding up the establishment of outbound connections.
|
||||
|
||||
The router thus dials a peer whenever there are: (i) a candidate peer to be
|
||||
consumed and (ii) a dialing routine is available in the pool.
|
||||
Given the size of the thread pool, the router is in practice is expected to
|
||||
dial in parallel all candidate peers produced by the peer manager.
|
||||
|
||||
> There was a random-interval sleep between starting subsequent dialing
|
||||
> routines. This behavior was removed by [#8839](https://github.com/tendermint/tendermint/pull/8839).
|
||||
|
||||
The dialing routine selected to dial to a peer runs by the `connectPeer()`
|
||||
method, which:
|
||||
|
||||
1. Calls `dialPeer()` to establish a connection with the remote peer
|
||||
1. In case of errors, invokes the `DialFailed` transition of the peer manager
|
||||
1. Calls `handshakePeer()` with the established connection and the expected remote node ID
|
||||
1. In case of errors, invokes the `DialFailed` transition of the peer manager
|
||||
1. Reports the established outbound connection through the `Dialed` transition of the peer manager
|
||||
1. In the transition fails, the established connection was refused
|
||||
1. Spawns a `routePeer()` routine for the peer
|
||||
|
||||
> Step 3. above acquires a mutex, preventing concurrent calls from different threads.
|
||||
> The reason is not clear, as all peer manager transitions are also protected by a mutex.
|
||||
>
|
||||
> Step 3i. above also notifies the peer manager's next peer to dial routine.
|
||||
> This should trigger the peer manager to produce another candidate peer.
|
||||
> TODO: check when this was introduced, as it breaks modularity.
|
||||
|
||||
In case of any of the above errors, the connection with the remote peer is
|
||||
**closed** and the dialing routines returns.
|
||||
|
||||
## Accepting peers
|
||||
|
||||
The router maintains a persistent routine `acceptPeers()` consuming connections
|
||||
accepted by each of the configured transports.
|
||||
|
||||
Each accepted connection is handled by a different `openConnection()` routine,
|
||||
spawned for this purpose, that operate as follows.
|
||||
There is no limit for the number of concurrent routines accepting peer's connections.
|
||||
|
||||
1. Calls `filterPeersIP()` with the peer address
|
||||
1. If the peer IP is rejected, the method returns
|
||||
1. Calls `handshakePeer()` with the accepted connection to retrieve the remote node ID
|
||||
1. If the handshake fails, the method returns
|
||||
1. Calls `filterPeersID()` with the peer ID (learned from the handshake)
|
||||
1. If the peer ID is rejected, the method returns
|
||||
1. Reports the established incoming connection through the `Accepted` transition of the peer manager
|
||||
1. In the transition fails, the accepted connection was refused and the method returns
|
||||
1. Switches to the `routePeer()` routine for the accepted peer
|
||||
|
||||
> Step 4. above acquires a mutex, preventing concurrent calls from different threads.
|
||||
> The reason is not clear, as all peer manager transitions are also protected by a mutex.
|
||||
|
||||
In case of any of the above errors, the connection with the remote peer is
|
||||
**closed**.
|
||||
|
||||
> TODO: Step 2. above has a limitation, commented in the source code, referring
|
||||
> to absence of an ack/nack in the handshake, which may case further
|
||||
> connections to be rejected.
|
||||
|
||||
> TODO: there is a `connTracker` in the router that rate limits addresses that
|
||||
> try to establish connections to often. This procedure should be documented.
|
||||
|
||||
## Evicting peers
|
||||
|
||||
The router maintains a persistent routine `evictPeers()` consuming
|
||||
[peers to evict](./peer_manager.md#evictnext-transition)
|
||||
produced by the peer manager.
|
||||
|
||||
The eviction of a peer is performed by closing the send queue associated to the peer.
|
||||
This queue maintains outbound messages destined to the peer, consumed by the
|
||||
peer's send routine.
|
||||
When the peer's send queue is closed, the peer's send routine is interrupted
|
||||
with no errors.
|
||||
|
||||
When the [routing messages](#routing-messages) routine notices that the peer's
|
||||
send routine was interrupted, it forces the interruption of peer's receive routine.
|
||||
When both send and receive routines are interrupted, the router considers the
|
||||
peer as disconnected, and its eviction has been done.
|
||||
|
||||
## Routing messages
|
||||
|
||||
When the router successfully establishes a connection with a peer, because it
|
||||
dialed the peer or accepted a connection from the peer, it starts routing
|
||||
messages from and to the peer.
|
||||
|
||||
This role is implemented by the `routePeer()` routine.
|
||||
Initially, the router notifies the peer manager that the peer is
|
||||
[`Ready`](./peer_manager.md#ready-transition).
|
||||
This notification includes the list of channels IDs supported by the peer,
|
||||
information obtained during the handshake process.
|
||||
|
||||
Then, the peer's send and receive routines are spawned.
|
||||
The send routine receives the peer ID, the established connection, and a new
|
||||
send queue associated with the peer.
|
||||
The peer's send queue is fed with messages produced by reactors and destined to
|
||||
the peer, which are sent to the peer through the established connection.
|
||||
The receive routine receives the peer ID and the established connection.
|
||||
Messages received through the established connections are forwarded to the
|
||||
appropriate reactors, using message queues associated to each channel ID.
|
||||
|
||||
From this point, the routing routine will monitor the peer's send and receive routines.
|
||||
When one of them returns, due to errors or because it was interrupted, the
|
||||
router forces the interruption of the other.
|
||||
To force the interruption of the send routine, the router closes the peer's
|
||||
send queue. To force the interruption of the receive routine, the router closes
|
||||
the connection established with the peer.
|
||||
|
||||
Finally, when both peer's send and receive routine return, the router notifies
|
||||
the peer manager that the peer is [`Disconnected`](./peer_manager.md#disconnected-transition).
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/tendermint/tendermint/abci/example/code"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmstrings "github.com/tendermint/tendermint/internal/libs/strings"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
@@ -484,7 +485,10 @@ func (app *Application) ExtendVote(_ context.Context, req *abci.RequestExtendVot
|
||||
time.Sleep(time.Duration(app.cfg.VoteExtensionDelayMS) * time.Millisecond)
|
||||
}
|
||||
|
||||
app.logger.Info("generated vote extension", "num", num, "ext", fmt.Sprintf("%x", ext[:extLen]), "state.Height", app.state.Height)
|
||||
app.logger.Info("generated vote extension",
|
||||
"num", num,
|
||||
"ext", tmstrings.LazySprintf("%x", ext[:extLen]),
|
||||
"state.Height", app.state.Height)
|
||||
return &abci.ResponseExtendVote{
|
||||
VoteExtension: ext[:extLen],
|
||||
}, nil
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -27,6 +26,6 @@ func Cleanup(ctx context.Context, logger log.Logger, testnetDir string, ti infra
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Removing testnet directory %q", testnetDir))
|
||||
logger.Info("Removing testnet", "directory", testnetDir)
|
||||
return os.RemoveAll(testnetDir)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ const (
|
||||
)
|
||||
|
||||
func TestBasicPartSet(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// Construct random data of size partSize * 100
|
||||
nParts := 100
|
||||
data := tmrand.Bytes(testPartSize * nParts)
|
||||
@@ -64,6 +68,10 @@ func TestBasicPartSet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWrongProof(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
// Construct random data of size partSize * 100
|
||||
data := tmrand.Bytes(testPartSize * 100)
|
||||
partSet := NewPartSetFromData(data, testPartSize)
|
||||
@@ -89,6 +97,10 @@ func TestWrongProof(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPartSetHeaderValidateBasic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
malleatePartSetHeader func(*PartSetHeader)
|
||||
@@ -110,6 +122,10 @@ func TestPartSetHeaderValidateBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPartValidateBasic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
malleatePart func(*Part)
|
||||
|
||||
@@ -1207,6 +1207,10 @@ func applyChangesToValSet(t *testing.T, expErr error, valSet *ValidatorSet, vals
|
||||
}
|
||||
|
||||
func TestValSetUpdatePriorityOrderTests(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
const nMaxElections int32 = 5000
|
||||
|
||||
testCases := []testVSetCfg{
|
||||
|
||||
Reference in New Issue
Block a user