From cce0a3c17145e10b4a73e3b564ecfd789c308acf Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Thu, 19 Aug 2021 16:25:17 -0400 Subject: [PATCH 01/12] docs: add package godoc for indexer (#6839) * docs: add indexer godoc * docs++ * docs++ * docs++ * docs++ * docs++ * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * Update state/indexer/doc.go Co-authored-by: M. J. Fromberger * docs++ Co-authored-by: M. J. Fromberger --- state/indexer/doc.go | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 state/indexer/doc.go diff --git a/state/indexer/doc.go b/state/indexer/doc.go new file mode 100644 index 000000000..61adbabac --- /dev/null +++ b/state/indexer/doc.go @@ -0,0 +1,72 @@ +/* +Package indexer defines Tendermint's block and transaction event indexing logic. + +Tendermint supports two primary means of block and transaction event indexing: + +1. A key-value sink via an embedded database with a proprietary query language. +2. A Postgres-based sink. + +An ABCI application can emit events during block and transaction execution in the form + + .= + +for example "transfer.amount=10000". + +An operator can enable one or both of the supported indexing sinks via the +'tx-index.indexer' Tendermint configuration. + +Example: + + [tx-index] + indexer = ["kv", "psql"] + +If an operator wants to completely disable indexing, they may simply just provide +the "null" sink option in the configuration. All other sinks will be ignored if +"null" is provided. + +If indexing is enabled, the indexer.Service will iterate over all enabled sinks +and invoke block and transaction indexing via the appropriate IndexBlockEvents +and IndexTxEvents methods. + +Note, the "kv" sink is considered deprecated and its query functionality is very +limited, but does allow users to directly query for block and transaction events +against Tendermint's RPC. Instead, operators are encouraged to use the "psql" +indexing sink when more complex queries are required and for reliability purposes +as PostgreSQL can scale. + +Prior to starting Tendermint with the "psql" indexing sink enabled, operators +must ensure the following: + +1. The "psql" indexing sink is provided in Tendermint's configuration. +2. A 'tx-index.psql-conn' value is provided that contains the PostgreSQL connection URI. +3. The block and transaction event schemas have been created in the PostgreSQL database. + +Tendermint provides the block and transaction event schemas in the following +path: state/indexer/sink/psql/schema.sql + +To create the schema in a PostgreSQL database, perform the schema query +manually or invoke schema creation via the CLI: + + $ psql -f state/indexer/sink/psql/schema.sql + +The "psql" indexing sink prohibits queries via RPC. When using a PostgreSQL sink, +queries can and should be made directly against the database using SQL. + +The following are some example SQL queries against the database schema: + +* Query for all transaction events for a given transaction hash: + + SELECT * FROM tx_events WHERE hash = '3E7D1F...'; + +* Query for all transaction events for a given block height: + + SELECT * FROM tx_events WHERE height = 25; + +* Query for transaction events that have a given type (i.e. value wildcard): + + SELECT * FROM tx_events WHERE key LIKE '%transfer.recipient%'; + +Note that if a complete abci.TxResult is needed, you will need to join "tx_events" with +"tx_results" via a foreign key, to obtain contains the raw protobuf-encoded abci.TxResult. +*/ +package indexer From 3a234e1144bb22487f51235b3c49d26b981290d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Aug 2021 13:32:11 +0200 Subject: [PATCH 02/12] build(deps): Bump docker/build-push-action from 2.6.1 to 2.7.0 (#6845) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.6.1 to 2.7.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v2.6.1...v2.7.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/proto-docker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 009f16898..89797a581 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -50,7 +50,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Publish to Docker Hub - uses: docker/build-push-action@v2.6.1 + uses: docker/build-push-action@v2.7.0 with: context: . file: ./DOCKER/Dockerfile diff --git a/.github/workflows/proto-docker.yml b/.github/workflows/proto-docker.yml index 8dc612602..ed31025b9 100644 --- a/.github/workflows/proto-docker.yml +++ b/.github/workflows/proto-docker.yml @@ -43,7 +43,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Publish to Docker Hub - uses: docker/build-push-action@v2.6.1 + uses: docker/build-push-action@v2.7.0 with: context: ./tools/proto file: ./tools/proto/Dockerfile From a374f74f7c914a3c7640694ae80c33bf74e05688 Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Fri, 20 Aug 2021 13:26:04 -0400 Subject: [PATCH 03/12] e2e: cleanup node start function (#6842) I realized after my last commit that my change made a following line of code a bit redundant. (alternatively my last change was redunadnt to the existing code.) I took this oppertunity to make some minor cleanups and logging changes to the node changes which I hope will make tests a bit more clear. --- test/e2e/runner/start.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/e2e/runner/start.go b/test/e2e/runner/start.go index ca3ebfcbd..c8d6163ed 100644 --- a/test/e2e/runner/start.go +++ b/test/e2e/runner/start.go @@ -9,6 +9,9 @@ import ( ) func Start(testnet *e2e.Testnet) error { + if len(testnet.Nodes) == 0 { + return fmt.Errorf("no nodes in testnet") + } // Nodes are already sorted by name. Sort them by name then startAt, // which gives the overall order startAt, mode, name. @@ -29,9 +32,7 @@ func Start(testnet *e2e.Testnet) error { sort.SliceStable(nodeQueue, func(i, j int) bool { return nodeQueue[i].StartAt < nodeQueue[j].StartAt }) - if len(nodeQueue) == 0 { - return fmt.Errorf("no nodes in testnet") - } + if nodeQueue[0].StartAt > 0 { return fmt.Errorf("no initial nodes in testnet") } @@ -93,10 +94,8 @@ func Start(testnet *e2e.Testnet) error { } } - logger.Info(fmt.Sprintf("Starting node %v at height %v...", node.Name, node.StartAt)) - if _, _, err := waitForHeight(testnet, node.StartAt); err != nil { - return err - } + logger.Info("Starting catch up node", "node", node.Name, "height", node.StartAt) + if err := execCompose(testnet.Dir, "up", "-d", node.Name); err != nil { return err } From 8700ca9d1a0c74b10386657591720953f8f9dfd9 Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:07:20 -0400 Subject: [PATCH 04/12] ADR 071: Proposer-based Timestamps (#6799) Architectural decision record for Proposer-based timestamps. --- .../adr-071-proposer-based-timestamps.md | 445 ++++++++++++++++++ docs/architecture/img/pbts-message.png | Bin 0 -> 32028 bytes 2 files changed, 445 insertions(+) create mode 100644 docs/architecture/adr-071-proposer-based-timestamps.md create mode 100644 docs/architecture/img/pbts-message.png diff --git a/docs/architecture/adr-071-proposer-based-timestamps.md b/docs/architecture/adr-071-proposer-based-timestamps.md new file mode 100644 index 000000000..c23488005 --- /dev/null +++ b/docs/architecture/adr-071-proposer-based-timestamps.md @@ -0,0 +1,445 @@ +# ADR 71: Proposer-Based Timestamps + +* [Changelog](#changelog) +* [Status](#status) +* [Context](#context) +* [Alternative Approaches](#alternative-approaches) + * [Remove timestamps altogether](#remove-timestamps-altogether) +* [Decision](#decision) +* [Detailed Design](#detailed-design) + * [Overview](#overview) + * [Proposal Timestamp and Block Timestamp](#proposal-timestamp-and-block-timestamp) + * [Saving the timestamp across heights](#saving-the-timestamp-across-heights) + * [Changes to `CommitSig`](#changes-to-commitsig) + * [Changes to `Commit`](#changes-to-commit) + * [Changes to `Vote` messages](#changes-to-vote-messages) + * [New consensus parameters](#new-consensus-parameters) + * [Changes to `Header`](#changes-to-header) + * [Changes to the block proposal step](#changes-to-the-block-proposal-step) + * [Proposer selects proposal timestamp](#proposer-selects-proposal-timestamp) + * [Proposer selects block timestamp](#proposer-selects-block-timestamp) + * [Proposer waits](#proposer-waits) + * [Changes to the propose step timeout](#changes-to-the-propose-step-timeout) + * [Changes to validation rules](#changes-to-validation-rules) + * [Proposal timestamp validation](#proposal-timestamp-validation) + * [Block timestamp validation](#block-timestamp-validation) + * [Changes to the prevote step](#changes-to-the-prevote-step) + * [Changes to the precommit step](#changes-to-the-precommit-step) + * [Changes to locking a block](#changes-to-locking-a-block) + * [Remove voteTime Completely](#remove-votetime-completely) +* [Future Improvements](#future-improvements) +* [Consequences](#consequences) + * [Positive](#positive) + * [Neutral](#neutral) + * [Negative](#negative) +* [References](#references) + +## Changelog + + - July 15 2021: Created by @williambanfield + - Aug 4 2021: Draft completed by @williambanfield + - Aug 5 2021: Draft updated to include data structure changes by @williambanfield + - Aug 20 2021: Language edits completed by @williambanfield + +## Status + + **Accepted** + +## Context + +Tendermint currently provides a monotonically increasing source of time known as [BFTTime](https://github.com/tendermint/spec/blob/master/spec/consensus/bft-time.md). +This mechanism for producing a source of time is reasonably simple. +Each correct validator adds a timestamp to each `Precommit` message it sends. +The timestamp it sends is either the validator's current known Unix time or one millisecond greater than the previous block time, depending on which value is greater. +When a block is produced, the proposer chooses the block timestamp as the weighted median of the times in all of the `Precommit` messages the proposer received. +The weighting is proportional to the amount of voting power, or stake, a validator has on the network. +This mechanism for producing timestamps is both deterministic and byzantine fault tolerant. + +This current mechanism for producing timestamps has a few drawbacks. +Validators do not have to agree at all on how close the selected block timestamp is to their own currently known Unix time. +Additionally, any amount of voting power `>1/3` may directly control the block timestamp. +As a result, it is quite possible that the timestamp is not particularly meaningful. + +These drawbacks present issues in the Tendermint protocol. +Timestamps are used by light clients to verify blocks. +Light clients rely on correspondence between their own currently known Unix time and the block timestamp to verify blocks they see; +However, their currently known Unix time may be greatly divergent from the block timestamp as a result of the limitations of `BFTTime`. + +The proposer-based timestamps specification suggests an alternative approach for producing block timestamps that remedies these issues. +Proposer-based timestamps alter the current mechanism for producing block timestamps in two main ways: + +1. The block proposer is amended to offer up its currently known Unix time as the timestamp for the next block. +1. Correct validators only approve the proposed block timestamp if it is close enough to their own currently known Unix time. + +The result of these changes is a more meaningful timestamp that cannot be controlled by `<= 2/3` of the validator voting power. +This document outlines the necessary code changes in Tendermint to implement the corresponding [proposer-based timestamps specification](https://github.com/tendermint/spec/tree/master/spec/consensus/proposer-based-timestamp). + +## Alternative Approaches + +### Remove timestamps altogether + +Computer clocks are bound to skew for a variety of reasons. +Using timestamps in our protocol means either accepting the timestamps as not reliable or impacting the protocol’s liveness guarantees. +This design requires impacting the protocol’s liveness in order to make the timestamps more reliable. +An alternate approach is to remove timestamps altogether from the block protocol. +`BFTTime` is deterministic but may be arbitrarily inaccurate. +However, having a reliable source of time is quite useful for applications and protocols built on top of a blockchain. + +We therefore decided not to remove the timestamp. +Applications often wish for some transactions to occur on a certain day, on a regular period, or after some time following a different event. +All of these require some meaningful representation of agreed upon time. +The following protocols and application features require a reliable source of time: +* Tendermint Light Clients [rely on correspondence between their known time](https://github.com/tendermint/spec/blob/master/spec/light-client/verification/README.md#definitions-1) and the block time for block verification. +* Tendermint Evidence validity is determined [either in terms of heights or in terms of time](https://github.com/tendermint/spec/blob/8029cf7a0fcc89a5004e173ec065aa48ad5ba3c8/spec/consensus/evidence.md#verification). +* Unbonding of staked assets in the Cosmos Hub [occurs after a period of 21 days](https://github.com/cosmos/governance/blob/ce75de4019b0129f6efcbb0e752cd2cc9e6136d3/params-change/Staking.md#unbondingtime). +* IBC packets can use either a [timestamp or a height to timeout packet delivery](https://docs.cosmos.network/v0.43/ibc/overview.html#acknowledgements). + +Finally, inflation distribution in the Cosmos Hub uses an approximation of time to calculate an annual percentage rate. +This approximation of time is calculated using [block heights with an estimated number of blocks produced in a year](https://github.com/cosmos/governance/blob/master/params-change/Mint.md#blocksperyear). +Proposer-based timestamps will allow this inflation calculation to use a more meaningful and accurate source of time. + + +## Decision + +Implement proposer-based timestamps and remove `BFTTime`. + +## Detailed Design + +### Overview + +Implementing proposer-based timestamps will require a few changes to Tendermint’s code. +These changes will be to the following components: +* The `internal/consensus/` package. +* The `state/` package. +* The `Vote`, `CommitSig`, `Commit` and `Header` types. +* The consensus parameters. + +### Proposal Timestamp and Block Timestamp + +This design discusses two timestamps: (1) The timestamp in the block and (2) the timestamp in the proposal message. +The existence and use of both of these timestamps can get a bit confusing, so some background is given here to clarify their uses. + +The [proposal message currently has a timestamp](https://github.com/tendermint/tendermint/blob/e5312942e30331e7c42b75426da2c6c9c00ae476/types/proposal.go#L31). +This timestamp is the current Unix time known to the proposer when sending the `Proposal` message. +This timestamp is not currently used as part of consensus. +The changes in this ADR will begin using the proposal message timestamp as part of consensus. +We will refer to this as the **proposal timestamp** throughout this design. + +The block has a timestamp field [in the header](https://github.com/tendermint/tendermint/blob/dc7c212c41a360bfe6eb38a6dd8c709bbc39aae7/types/block.go#L338). +This timestamp is set currently as part of Tendermint’s `BFTtime` algorithm. +It is set when a block is proposed and it is checked by the validators when they are deciding to prevote the block. +This field will continue to be used but the logic for creating and validating this timestamp will change. +We will refer to this as the **block timestamp** throughout this design. + +At a high level, the proposal timestamp from height `H` is used as the block timestamp at height `H+1`. +The following image shows this relationship. +The rest of this document describes the code changes that will make this possible. + +![](./img/pbts-message.png) + +### Saving the timestamp across heights + +Currently, `BFTtime` uses `LastCommit` to construct the block timestamp. +The `LastCommit` is created at height `H-1` and is saved in the state store to be included in the block at height `H`. +`BFTtime` takes the weighted median of the timestamps in `LastCommit.CommitSig` to build the timestamp for height `H`. + +For proposer-based timestamps, the `LastCommit.CommitSig` timestamps will no longer be used to build the timestamps for height `H`. +Instead, the proposal timestamp from height `H-1` will become the block timestamp for height `H`. +To enable this, we will add a `Timestamp` field to the `Commit` struct. +This field will be populated at each height with the proposal timestamp decided on at the previous height. +This timestamp will also be saved with the rest of the commit in the state store [when the commit is finalized](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/internal/consensus/state.go#L1611) so that it can be recovered if Tendermint crashes. +Changes to the `CommitSig` and `Commit` struct are detailed below. + +### Changes to `CommitSig` + +The [CommitSig](https://github.com/tendermint/tendermint/blob/a419f4df76fe4aed668a6c74696deabb9fe73211/types/block.go#L604) struct currently contains a timestamp. +This timestamp is the current Unix time known to the validator when it issued a `Precommit` for the block. +This timestamp is no longer used and will be removed in this change. + +`CommitSig` will be updated as follows: + +```diff +type CommitSig struct { + BlockIDFlag BlockIDFlag `json:"block_id_flag"` + ValidatorAddress Address `json:"validator_address"` +-- Timestamp time.Time `json:"timestamp"` + Signature []byte `json:"signature"` +} +``` + +### Changes to `Commit` + +The [Commit](https://github.com/tendermint/tendermint/blob/a419f4df76fe4aed668a6c74696deabb9fe73211/types/block.go#L746) struct does not currently contain a timestamp. +The timestamps in the `Commit.CommitSig` entries are currently used to build the block timestamp. +With these timestamps removed, the commit time will instead be stored in the `Commit` struct. + +`Commit` will be updated as follows. + +```diff +type Commit struct { + Height int64 `json:"height"` + Round int32 `json:"round"` +++ Timestamp time.Time `json:"timestamp"` + BlockID BlockID `json:"block_id"` + Signatures []CommitSig `json:"signatures"` +} +``` + +### Changes to `Vote` messages + +`Precommit` and `Prevote` messages use a common [Vote struct](https://github.com/tendermint/tendermint/blob/a419f4df76fe4aed668a6c74696deabb9fe73211/types/vote.go#L50). +This struct currently contains a timestamp. +This timestamp is set using the [voteTime](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/internal/consensus/state.go#L2241) function and therefore vote times correspond to the current Unix time known to the validator. +For precommits, this timestamp is used to construct the [CommitSig that is included in the block in the LastCommit](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/types/block.go#L754) field. +For prevotes, this field is unused. +Proposer-based timestamps will use the [RoundState.Proposal](https://github.com/tendermint/tendermint/blob/c3ae6f5b58e07b29c62bfdc5715b6bf8ae5ee951/internal/consensus/types/round_state.go#L76) timestamp to construct the `signedBytes` `CommitSig`. +This timestamp is therefore no longer useful and will be dropped. + +`Vote` will be updated as follows: + +```diff +type Vote struct { + Type tmproto.SignedMsgType `json:"type"` + Height int64 `json:"height"` + Round int32 `json:"round"` + BlockID BlockID `json:"block_id"` // zero if vote is nil. +-- Timestamp time.Time `json:"timestamp"` + ValidatorAddress Address `json:"validator_address"` + ValidatorIndex int32 `json:"validator_index"` + Signature []byte `json:"signature"` +} +``` + +### New consensus parameters + +The proposer-based timestamp specification includes multiple new parameters that must be the same among all validators. +These parameters are `PRECISION`, `MSGDELAY`, and `ACCURACY`. + +The `PRECISION` and `MSGDELAY` parameters are used to determine if the proposed timestamp is acceptable. +A validator will only Prevote a proposal if the proposal timestamp is considered `timely`. +A proposal timestamp is considered `timely` if it is within `PRECISION` and `MSGDELAY` of the Unix time known to the validator. +More specifically, a proposal timestamp is `timely` if `validatorLocalTime - PRECISION < proposalTime < validatorLocalTime + PRECISION + MSGDELAY`. + +Because the `PRECISION` and `MSGDELAY` parameters must be the same across all validators, they will be added to the [consensus parameters](https://github.com/tendermint/tendermint/blob/master/proto/tendermint/types/params.proto#L13) as [durations](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration). + +The proposer-based timestamp specification also includes a [new ACCURACY parameter](https://github.com/tendermint/spec/blob/master/spec/consensus/proposer-based-timestamp/pbts-sysmodel_001_draft.md#pbts-clocksync-external0). +Intuitively, `ACCURACY` represents the difference between the ‘real’ time and the currently known time of correct validators. +The currently known Unix time of any validator is always somewhat different from real time. +`ACCURACY` is the largest such difference between each validator's time and real time taken as an absolute value. +This is not something a computer can determine on its own and must be specified as an estimate by community running a Tendermint-based chain. +It is used in the new algorithm to [calculate a timeout for the propose step](https://github.com/tendermint/spec/blob/master/spec/consensus/proposer-based-timestamp/pbts-algorithm_001_draft.md#pbts-alg-startround0). +`ACCURACY` is assumed to be the same across all validators and therefore should be included as a consensus parameter. + +The consensus will be updated to include this `Timestamp` field as follows: + +```diff +type ConsensusParams struct { + Block BlockParams `json:"block"` + Evidence EvidenceParams `json:"evidence"` + Validator ValidatorParams `json:"validator"` + Version VersionParams `json:"version"` +++ Timestamp TimestampParams `json:"timestamp"` +} +``` + +```go +type TimestampParams struct { + Accuracy time.Duration `json:"accuracy"` + Precision time.Duration `json:"precision"` + MsgDelay time.Duration `json:"msg_delay"` +} +``` + +### Changes to `Header` + +The [Header](https://github.com/tendermint/tendermint/blob/a419f4df76fe4aed668a6c74696deabb9fe73211/types/block.go#L338) struct currently contains a timestamp. +This timestamp is set as the `BFTtime` derived from the block's `LastCommit.CommitSig` timestamps. +This timestamp will no longer be derived from the `LastCommit.CommitSig` timestamps and will instead be included directly into the block's `LastCommit`. +This timestamp will therfore be identical in both the `Header` and the `LastCommit`. +To clarify that the timestamp in the header corresponds to the `LastCommit`'s time, we will rename this timestamp field to `last_timestamp`. + +`Header` will be updated as follows: + +```diff +type Header struct { + // basic block info + Version version.Consensus `json:"version"` + ChainID string `json:"chain_id"` + Height int64 `json:"height"` +-- Time time.Time `json:"time"` +++ LastTimestamp time.Time `json:"last_timestamp"` + + // prev block info + LastBlockID BlockID `json:"last_block_id"` + + // hashes of block data + LastCommitHash tmbytes.HexBytes `json:"last_commit_hash"` + DataHash tmbytes.HexBytes `json:"data_hash"` + + // hashes from the app output from the prev block + ValidatorsHash tmbytes.HexBytes `json:"validators_hash"` + NextValidatorsHash tmbytes.HexBytes `json:"next_validators_hash"` + ConsensusHash tmbytes.HexBytes `json:"consensus_hash"` + AppHash tmbytes.HexBytes `json:"app_hash"` + + // root hash of all results from the txs from the previous block + LastResultsHash tmbytes.HexBytes `json:"last_results_hash"` + + // consensus info + EvidenceHash tmbytes.HexBytes `json:"evidence_hash"` + ProposerAddress Address `json:"proposer_address"` +} +``` + +### Changes to the block proposal step + +#### Proposer selects proposal timestamp + +The proposal logic already [sets the Unix time known to the validator](https://github.com/tendermint/tendermint/blob/2abfe20114ee3bb3adfee817589033529a804e4d/types/proposal.go#L44) into the `Proposal` message. +This satisfies the proposer-based timestamp specification and does not need to change. + +#### Proposer selects block timestamp + +The proposal timestamp that was decided in height `H-1` will be stored in the `State` struct's in the `RoundState.LastCommit` field. +The proposer will select this timestamp to use as the block timestamp at height `H`. + +#### Proposer waits + +Block timestamps must be monotonically increasing. +In `BFTTime`, if a validator’s clock was behind, the [validator added 1 millisecond to the previous block’s time and used that in its vote messages](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/internal/consensus/state.go#L2246). +A goal of adding proposer-based timestamps is to enforce some degree of clock synchronization, so having a mechanism that completely ignores the Unix time of the validator time no longer works. + +Validator clocks will not be perfectly in sync. +Therefore, the proposer’s current known Unix time may be less than the `LastCommit.Timestamp`. +If the proposer’s current known Unix time is less than the `LastCommit.Timestamp`, the proposer will sleep until its known Unix time exceeds `LastCommit.Timestamp`. + +This change will require amending the [defaultDecideProposal](https://github.com/tendermint/tendermint/blob/822893615564cb20b002dd5cf3b42b8d364cb7d9/internal/consensus/state.go#L1180) method. +This method should now block until the proposer’s time is greater than `LastCommit.Timestamp`. + +#### Changes to the propose step timeout + +Currently, a validator waiting for a proposal will proceed past the propose step if the configured propose timeout is reached and no proposal is seen. +Proposer-based timestamps requires changing this timeout logic. + +The proposer will now wait until its current known Unix time exceeds the `LastCommit.Timestamp` to propose a block. +The validators must now take this and some other factors into account when deciding when to timeout the propose step. +Specifically, the propose step timeout must also take into account potential inaccuracy in the validator’s clock and in the clock of the proposer. +Additionally, there may be a delay communicating the proposal message from the proposer to the other validators. + +Therefore, validators waiting for a proposal must wait until after the `LastCommit.Timestamp` before timing out. +To account for possible inaccuracy in its own clock, inaccuracy in the proposer’s clock, and message delay, validators waiting for a proposal will wait until `LastCommit.Timesatmp + 2*ACCURACY + MSGDELAY`. + The spec defines this as `waitingTime`. + +The [propose step’s timeout is set in enterPropose](https://github.com/tendermint/tendermint/blob/822893615564cb20b002dd5cf3b42b8d364cb7d9/internal/consensus/state.go#L1108) in `state.go`. +`enterPropose` will be changed to calculate waiting time using the new consensus parameters. +The timeout in `enterPropose` will then be set as the maximum of `waitingTime` and the [configured proposal step timeout](https://github.com/tendermint/tendermint/blob/dc7c212c41a360bfe6eb38a6dd8c709bbc39aae7/config/config.go#L1013). + +### Changes to validation rules + +The rules for validating that a proposal is valid will need slight modification to implement proposer-based timestamps. +Specifically, we will change the validation logic to ensure that the proposal timestamp is `timely` and we will modify the way the block timestamp is validated as well. + +#### Proposal timestamp validation + +Adding proposal timestamp validation is a reasonably straightforward change. +The current Unix time known to the proposer is already included in the [Proposal message](https://github.com/tendermint/tendermint/blob/dc7c212c41a360bfe6eb38a6dd8c709bbc39aae7/types/proposal.go#L31). +Once the proposal is received, the complete message is stored in the `RoundState.Proposal` field. +The precommit and prevote validation logic does not currently use this timestamp. +This validation logic will be updated to check that the proposal timestamp is within `PRECISION` of the current Unix time known to the validators. +If the timestamp is not within `PRECISION` of the current Unix time known to the validator, the proposal will not be considered it valid. +The validator will also check that the proposal time is greater than the block timestamp from the previous height. + +If no valid proposal is received by the proposal timeout, the validator will prevote nil. +This is identical to the current logic. + +#### Block timestamp validation + +The [validBlock function](https://github.com/tendermint/tendermint/blob/c3ae6f5b58e07b29c62bfdc5715b6bf8ae5ee951/state/validation.go#L14) currently [validates the proposed block timestamp in three ways](https://github.com/tendermint/tendermint/blob/c3ae6f5b58e07b29c62bfdc5715b6bf8ae5ee951/state/validation.go#L118). +First, the validation logic checks that this timestamp is greater than the previous block’s timestamp. +Additionally, it validates that the block timestamp is correctly calculated as the weighted median of the timestamps in the [block’s LastCommit](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/types/block.go#L48). +Finally, the logic also authenticates the timestamps in the `LastCommit`. +The cryptographic signature in each `CommitSig` is created by signing a hash of fields in the block with the validator’s private key. +One of the items in this `signedBytes` hash is derived from the timestamp in the `CommitSig`. +To authenticate the `CommitSig` timestamp, the validator builds a hash of fields that includes the timestamp and checks this hash against the provided signature. +This takes place in the [VerifyCommit function](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/types/validation.go#L25). + +The logic to validate that the block timestamp is greater than the previous block’s timestamp also works for proposer-based timestamps and will not change. + +`BFTTime` validation is no longer applicable and will be removed. +Validators will no longer check that the block timestamp is a weighted median of `LastCommit` timestamps. +This will mean removing the call to [MedianTime in the validateBlock function](https://github.com/tendermint/tendermint/blob/4db71da68e82d5cb732b235eeb2fd69d62114b45/state/validation.go#L117). +The `MedianTime` function can be completely removed. +The `LastCommit` timestamps may also be removed. + +The `signedBytes` validation logic in `VerifyCommit` will be slightly altered. +The `CommitSig`s in the block’s `LastCommit` will no longer each contain a timestamp. +The validation logic will instead include the `LastCommit.Timestamp` in the hash of fields for generating the `signedBytes`. +The cryptographic signatures included in the `CommitSig`s will then be checked against this `signedBytes` hash to authenticate the timestamp. +Specifically, the `VerifyCommit` function will be updated to use this new timestamp. + +### Changes to the prevote step + +Currently, a validator will prevote a proposal in one of three cases: + +* Case 1: Validator has no locked block and receives a valid proposal. +* Case 2: Validator has a locked block and receives a valid proposal matching its locked block. +* Case 3: Validator has a locked block, sees a valid proposal not matching its locked block but sees +⅔ prevotes for the new proposal’s block. + +The only change we will make to the prevote step is to what a validator considers a valid proposal as detailed above. + +### Changes to the precommit step + +The precommit step will not require much modification. +Its proposal validation rules will change in the same ways that validation will change in the prevote step. + +### Changes to locking a block +When a validator receives a valid proposed block and +2/3 prevotes for that block, it stores the block as its ‘locked block’ in the [RoundState.ValidBlock](https://github.com/tendermint/tendermint/blob/e8013281281985e3ada7819f42502b09623d24a0/internal/consensus/types/round_state.go#L85) field. +In each subsequent round it will prevote that block. +A validator will only change which block it has locked if it sees +2/3 prevotes for a different block. + +This mechanism will remain largely unchanged. +The only difference is the addition of proposal timestamp validation. +A validator will prevote nil in a round if the proposal message it received is not `timely`. +Prevoting nil in this case will not cause a validator to ‘unlock’ its locked block. +This difference is an incidental result of the changes to prevote validation. +It is included in this design for completeness and to clarify that no additional changes will be made to block locking. + +### Remove voteTime Completely + +[voteTime](https://github.com/tendermint/tendermint/blob/822893615564cb20b002dd5cf3b42b8d364cb7d9/internal/consensus/state.go#L2229) is a mechanism for calculating the next `BFTTime` given both the validator's current known Unix time and the previous block timestamp. +If the previous block timestamp is greater than the validator's current known Unix time, then voteTime returns a value one millisecond greater than the previous block timestamp. +This logic is used in multiple places and is no longer needed for proposer-based timestamps. +It should therefore be removed completely. + +## Future Improvements + +* Implement BLS signature aggregation. +By removing fields from the `Precommit` messages, we are able to aggregate signatures. + +## Consequences + +### Positive + +* `<2/3` of validators can no longer influence block timestamps. +* Block timestamp will have stronger correspondence to real time. +* Improves the reliability of light client block verification. +* Enables BLS signature aggregation. +* Enables evidence handling to use time instead of height for evidence validity. + +### Neutral + +* Alters Tendermint’s liveness properties. +Liveness now requires that all correct validators have synchronized clocks within a bound. +Liveness will now also require that validators’ clocks move forward, which was not required under `BFTTime`. + +### Negative + +* May increase the length of the propose step if there is a large skew between the previous proposer and the current proposer’s local Unix time. +This skew will be bound by the `PRECISION` value, so it is unlikely to be too large. + +* Current chains with block timestamps far in the future will either need to pause consensus until after the erroneous block timestamp or must maintain synchronized but very inaccurate clocks. + +## References + +* [PBTS Spec](https://github.com/tendermint/spec/tree/master/spec/consensus/proposer-based-timestamp) +* [BFTTime spec](https://github.com/tendermint/spec/blob/master/spec/consensus/bft-time.md) diff --git a/docs/architecture/img/pbts-message.png b/docs/architecture/img/pbts-message.png new file mode 100644 index 0000000000000000000000000000000000000000..400f3569021ca719fe06f20e0db17940091751a1 GIT binary patch literal 32028 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 literal 0 HcmV?d00001 From d0e33b429288fb7d284205732beb82b59b4dea76 Mon Sep 17 00:00:00 2001 From: Hlib Kanunnikov Date: Mon, 23 Aug 2021 17:45:08 +0300 Subject: [PATCH 05/12] blocksync: complete transition from Blockchain to BlockSync (#6847) --- internal/blocksync/v0/reactor.go | 118 +++++++++++++------------- internal/blocksync/v0/reactor_test.go | 38 ++++----- node/setup.go | 4 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/internal/blocksync/v0/reactor.go b/internal/blocksync/v0/reactor.go index c43959808..4ddfa4edc 100644 --- a/internal/blocksync/v0/reactor.go +++ b/internal/blocksync/v0/reactor.go @@ -29,10 +29,10 @@ var ( // TODO: Remove once p2p refactor is complete. // ref: https://github.com/tendermint/tendermint/issues/5670 ChannelShims = map[p2p.ChannelID]*p2p.ChannelDescriptorShim{ - BlockchainChannel: { + BlockSyncChannel: { MsgType: new(bcproto.Message), Descriptor: &p2p.ChannelDescriptor{ - ID: byte(BlockchainChannel), + ID: byte(BlockSyncChannel), Priority: 5, SendQueueCapacity: 1000, RecvBufferCapacity: 1024, @@ -44,8 +44,8 @@ var ( ) const ( - // BlockchainChannel is a channel for blocks and status updates - BlockchainChannel = p2p.ChannelID(0x40) + // BlockSyncChannel is a channel for blocks and status updates + BlockSyncChannel = p2p.ChannelID(0x40) trySyncIntervalMS = 10 @@ -60,7 +60,7 @@ const ( ) type consensusReactor interface { - // For when we switch from blockchain reactor and block sync to the consensus + // For when we switch from block sync reactor to the consensus // machine. SwitchToConsensus(state sm.State, skipWAL bool) } @@ -87,17 +87,17 @@ type Reactor struct { consReactor consensusReactor blockSync *tmSync.AtomicBool - blockchainCh *p2p.Channel - // blockchainOutBridgeCh defines a channel that acts as a bridge between sending Envelope - // messages that the reactor will consume in processBlockchainCh and receiving messages + blockSyncCh *p2p.Channel + // blockSyncOutBridgeCh defines a channel that acts as a bridge between sending Envelope + // messages that the reactor will consume in processBlockSyncCh and receiving messages // from the peer updates channel and other goroutines. We do this instead of directly - // sending on blockchainCh.Out to avoid race conditions in the case where other goroutines - // send Envelopes directly to the to blockchainCh.Out channel, since processBlockchainCh - // may close the blockchainCh.Out channel at the same time that other goroutines send to - // blockchainCh.Out. - blockchainOutBridgeCh chan p2p.Envelope - peerUpdates *p2p.PeerUpdates - closeCh chan struct{} + // sending on blockSyncCh.Out to avoid race conditions in the case where other goroutines + // send Envelopes directly to the to blockSyncCh.Out channel, since processBlockSyncCh + // may close the blockSyncCh.Out channel at the same time that other goroutines send to + // blockSyncCh.Out. + blockSyncOutBridgeCh chan p2p.Envelope + peerUpdates *p2p.PeerUpdates + closeCh chan struct{} requestsCh <-chan BlockRequest errorsCh <-chan peerError @@ -119,7 +119,7 @@ func NewReactor( blockExec *sm.BlockExecutor, store *store.BlockStore, consReactor consensusReactor, - blockchainCh *p2p.Channel, + blockSyncCh *p2p.Channel, peerUpdates *p2p.PeerUpdates, blockSync bool, metrics *cons.Metrics, @@ -137,23 +137,23 @@ func NewReactor( errorsCh := make(chan peerError, maxPeerErrBuffer) // NOTE: The capacity should be larger than the peer count. r := &Reactor{ - initialState: state, - blockExec: blockExec, - store: store, - pool: NewBlockPool(startHeight, requestsCh, errorsCh), - consReactor: consReactor, - blockSync: tmSync.NewBool(blockSync), - requestsCh: requestsCh, - errorsCh: errorsCh, - blockchainCh: blockchainCh, - blockchainOutBridgeCh: make(chan p2p.Envelope), - peerUpdates: peerUpdates, - closeCh: make(chan struct{}), - metrics: metrics, - syncStartTime: time.Time{}, + initialState: state, + blockExec: blockExec, + store: store, + pool: NewBlockPool(startHeight, requestsCh, errorsCh), + consReactor: consReactor, + blockSync: tmSync.NewBool(blockSync), + requestsCh: requestsCh, + errorsCh: errorsCh, + blockSyncCh: blockSyncCh, + blockSyncOutBridgeCh: make(chan p2p.Envelope), + peerUpdates: peerUpdates, + closeCh: make(chan struct{}), + metrics: metrics, + syncStartTime: time.Time{}, } - r.BaseService = *service.NewBaseService(logger, "Blockchain", r) + r.BaseService = *service.NewBaseService(logger, "BlockSync", r) return r, nil } @@ -174,7 +174,7 @@ func (r *Reactor) OnStart() error { go r.poolRoutine(false) } - go r.processBlockchainCh() + go r.processBlockSyncCh() go r.processPeerUpdates() return nil @@ -199,7 +199,7 @@ func (r *Reactor) OnStop() { // Wait for all p2p Channels to be closed before returning. This ensures we // can easily reason about synchronization of all p2p Channels and ensure no // panics will occur. - <-r.blockchainCh.Done() + <-r.blockSyncCh.Done() <-r.peerUpdates.Done() } @@ -214,7 +214,7 @@ func (r *Reactor) respondToPeer(msg *bcproto.BlockRequest, peerID types.NodeID) return } - r.blockchainCh.Out <- p2p.Envelope{ + r.blockSyncCh.Out <- p2p.Envelope{ To: peerID, Message: &bcproto.BlockResponse{Block: blockProto}, } @@ -223,16 +223,16 @@ func (r *Reactor) respondToPeer(msg *bcproto.BlockRequest, peerID types.NodeID) } r.Logger.Info("peer requesting a block we do not have", "peer", peerID, "height", msg.Height) - r.blockchainCh.Out <- p2p.Envelope{ + r.blockSyncCh.Out <- p2p.Envelope{ To: peerID, Message: &bcproto.NoBlockResponse{Height: msg.Height}, } } -// handleBlockchainMessage handles envelopes sent from peers on the -// BlockchainChannel. It returns an error only if the Envelope.Message is unknown +// handleBlockSyncMessage handles envelopes sent from peers on the +// BlockSyncChannel. It returns an error only if the Envelope.Message is unknown // for this channel. This should never be called outside of handleMessage. -func (r *Reactor) handleBlockchainMessage(envelope p2p.Envelope) error { +func (r *Reactor) handleBlockSyncMessage(envelope p2p.Envelope) error { logger := r.Logger.With("peer", envelope.From) switch msg := envelope.Message.(type) { @@ -249,7 +249,7 @@ func (r *Reactor) handleBlockchainMessage(envelope p2p.Envelope) error { r.pool.AddBlock(envelope.From, block, block.Size()) case *bcproto.StatusRequest: - r.blockchainCh.Out <- p2p.Envelope{ + r.blockSyncCh.Out <- p2p.Envelope{ To: envelope.From, Message: &bcproto.StatusResponse{ Height: r.store.Height(), @@ -288,8 +288,8 @@ func (r *Reactor) handleMessage(chID p2p.ChannelID, envelope p2p.Envelope) (err r.Logger.Debug("received message", "message", envelope.Message, "peer", envelope.From) switch chID { - case BlockchainChannel: - err = r.handleBlockchainMessage(envelope) + case BlockSyncChannel: + err = r.handleBlockSyncMessage(envelope) default: err = fmt.Errorf("unknown channel ID (%d) for envelope (%v)", chID, envelope) @@ -298,30 +298,30 @@ func (r *Reactor) handleMessage(chID p2p.ChannelID, envelope p2p.Envelope) (err return err } -// processBlockchainCh initiates a blocking process where we listen for and handle -// envelopes on the BlockchainChannel and blockchainOutBridgeCh. Any error encountered during -// message execution will result in a PeerError being sent on the BlockchainChannel. +// processBlockSyncCh initiates a blocking process where we listen for and handle +// envelopes on the BlockSyncChannel and blockSyncOutBridgeCh. Any error encountered during +// message execution will result in a PeerError being sent on the BlockSyncChannel. // When the reactor is stopped, we will catch the signal and close the p2p Channel // gracefully. -func (r *Reactor) processBlockchainCh() { - defer r.blockchainCh.Close() +func (r *Reactor) processBlockSyncCh() { + defer r.blockSyncCh.Close() for { select { - case envelope := <-r.blockchainCh.In: - if err := r.handleMessage(r.blockchainCh.ID, envelope); err != nil { - r.Logger.Error("failed to process message", "ch_id", r.blockchainCh.ID, "envelope", envelope, "err", err) - r.blockchainCh.Error <- p2p.PeerError{ + case envelope := <-r.blockSyncCh.In: + if err := r.handleMessage(r.blockSyncCh.ID, envelope); err != nil { + r.Logger.Error("failed to process message", "ch_id", r.blockSyncCh.ID, "envelope", envelope, "err", err) + r.blockSyncCh.Error <- p2p.PeerError{ NodeID: envelope.From, Err: err, } } - case envelope := <-r.blockchainOutBridgeCh: - r.blockchainCh.Out <- envelope + case envelope := <-r.blockSyncOutBridgeCh: + r.blockSyncCh.Out <- envelope case <-r.closeCh: - r.Logger.Debug("stopped listening on blockchain channel; closing...") + r.Logger.Debug("stopped listening on block sync channel; closing...") return } @@ -340,7 +340,7 @@ func (r *Reactor) processPeerUpdate(peerUpdate p2p.PeerUpdate) { switch peerUpdate.Status { case p2p.PeerStatusUp: // send a status update the newly added peer - r.blockchainOutBridgeCh <- p2p.Envelope{ + r.blockSyncOutBridgeCh <- p2p.Envelope{ To: peerUpdate.NodeID, Message: &bcproto.StatusResponse{ Base: r.store.Base(), @@ -406,13 +406,13 @@ func (r *Reactor) requestRoutine() { return case request := <-r.requestsCh: - r.blockchainOutBridgeCh <- p2p.Envelope{ + r.blockSyncOutBridgeCh <- p2p.Envelope{ To: request.PeerID, Message: &bcproto.BlockRequest{Height: request.Height}, } case pErr := <-r.errorsCh: - r.blockchainCh.Error <- p2p.PeerError{ + r.blockSyncCh.Error <- p2p.PeerError{ NodeID: pErr.peerID, Err: pErr.err, } @@ -423,7 +423,7 @@ func (r *Reactor) requestRoutine() { go func() { defer r.poolWG.Done() - r.blockchainOutBridgeCh <- p2p.Envelope{ + r.blockSyncOutBridgeCh <- p2p.Envelope{ Broadcast: true, Message: &bcproto.StatusRequest{}, } @@ -554,14 +554,14 @@ FOR_LOOP: // NOTE: We've already removed the peer's request, but we still need // to clean up the rest. peerID := r.pool.RedoRequest(first.Height) - r.blockchainCh.Error <- p2p.PeerError{ + r.blockSyncCh.Error <- p2p.PeerError{ NodeID: peerID, Err: err, } peerID2 := r.pool.RedoRequest(second.Height) if peerID2 != peerID { - r.blockchainCh.Error <- p2p.PeerError{ + r.blockSyncCh.Error <- p2p.PeerError{ NodeID: peerID2, Err: err, } diff --git a/internal/blocksync/v0/reactor_test.go b/internal/blocksync/v0/reactor_test.go index e038b57af..a1ddc02cd 100644 --- a/internal/blocksync/v0/reactor_test.go +++ b/internal/blocksync/v0/reactor_test.go @@ -32,9 +32,9 @@ type reactorTestSuite struct { reactors map[types.NodeID]*Reactor app map[types.NodeID]proxy.AppConns - blockchainChannels map[types.NodeID]*p2p.Channel - peerChans map[types.NodeID]chan p2p.PeerUpdate - peerUpdates map[types.NodeID]*p2p.PeerUpdates + blockSyncChannels map[types.NodeID]*p2p.Channel + peerChans map[types.NodeID]chan p2p.PeerUpdate + peerUpdates map[types.NodeID]*p2p.PeerUpdates blockSync bool } @@ -53,19 +53,19 @@ func setup( "must specify at least one block height (nodes)") rts := &reactorTestSuite{ - logger: log.TestingLogger().With("module", "blockchain", "testCase", t.Name()), - network: p2ptest.MakeNetwork(t, p2ptest.NetworkOptions{NumNodes: numNodes}), - nodes: make([]types.NodeID, 0, numNodes), - reactors: make(map[types.NodeID]*Reactor, numNodes), - app: make(map[types.NodeID]proxy.AppConns, numNodes), - blockchainChannels: make(map[types.NodeID]*p2p.Channel, numNodes), - peerChans: make(map[types.NodeID]chan p2p.PeerUpdate, numNodes), - peerUpdates: make(map[types.NodeID]*p2p.PeerUpdates, numNodes), - blockSync: true, + logger: log.TestingLogger().With("module", "block_sync", "testCase", t.Name()), + network: p2ptest.MakeNetwork(t, p2ptest.NetworkOptions{NumNodes: numNodes}), + nodes: make([]types.NodeID, 0, numNodes), + reactors: make(map[types.NodeID]*Reactor, numNodes), + app: make(map[types.NodeID]proxy.AppConns, numNodes), + blockSyncChannels: make(map[types.NodeID]*p2p.Channel, numNodes), + peerChans: make(map[types.NodeID]chan p2p.PeerUpdate, numNodes), + peerUpdates: make(map[types.NodeID]*p2p.PeerUpdates, numNodes), + blockSync: true, } - chDesc := p2p.ChannelDescriptor{ID: byte(BlockchainChannel)} - rts.blockchainChannels = rts.network.MakeChannelsNoCleanup(t, chDesc, new(bcproto.Message), int(chBuf)) + chDesc := p2p.ChannelDescriptor{ID: byte(BlockSyncChannel)} + rts.blockSyncChannels = rts.network.MakeChannelsNoCleanup(t, chDesc, new(bcproto.Message), int(chBuf)) i := 0 for nodeID := range rts.network.Nodes { @@ -161,7 +161,7 @@ func (rts *reactorTestSuite) addNode(t *testing.T, blockExec, blockStore, nil, - rts.blockchainChannels[nodeID], + rts.blockSyncChannels[nodeID], rts.peerUpdates[nodeID], rts.blockSync, cons.NopMetrics()) @@ -181,7 +181,7 @@ func (rts *reactorTestSuite) start(t *testing.T) { } func TestReactor_AbruptDisconnect(t *testing.T) { - config := cfg.ResetTestRoot("blockchain_reactor_test") + config := cfg.ResetTestRoot("block_sync_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := factory.RandGenesisDoc(config, 1, false, 30) @@ -216,7 +216,7 @@ func TestReactor_AbruptDisconnect(t *testing.T) { } func TestReactor_SyncTime(t *testing.T) { - config := cfg.ResetTestRoot("blockchain_reactor_test") + config := cfg.ResetTestRoot("block_sync_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := factory.RandGenesisDoc(config, 1, false, 30) @@ -239,7 +239,7 @@ func TestReactor_SyncTime(t *testing.T) { } func TestReactor_NoBlockResponse(t *testing.T) { - config := cfg.ResetTestRoot("blockchain_reactor_test") + config := cfg.ResetTestRoot("block_sync_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := factory.RandGenesisDoc(config, 1, false, 30) @@ -286,7 +286,7 @@ func TestReactor_BadBlockStopsPeer(t *testing.T) { // See: https://github.com/tendermint/tendermint/issues/6005 t.SkipNow() - config := cfg.ResetTestRoot("blockchain_reactor_test") + config := cfg.ResetTestRoot("block_sync_reactor_test") defer os.RemoveAll(config.RootDir) maxBlockHeight := int64(48) diff --git a/node/setup.go b/node/setup.go index ceadcd688..e9bfb029a 100644 --- a/node/setup.go +++ b/node/setup.go @@ -362,7 +362,7 @@ func createBlockchainReactor( reactor, err := bcv0.NewReactor( logger, state.Copy(), blockExec, blockStore, csReactor, - channels[bcv0.BlockchainChannel], peerUpdates, blockSync, + channels[bcv0.BlockSyncChannel], peerUpdates, blockSync, metrics, ) if err != nil { @@ -727,7 +727,7 @@ func makeNodeInfo( var bcChannel byte switch config.BlockSync.Version { case cfg.BlockSyncV0: - bcChannel = byte(bcv0.BlockchainChannel) + bcChannel = byte(bcv0.BlockSyncChannel) case cfg.BlockSyncV2: bcChannel = bcv2.BlockchainChannel From 39dee8abc576cbf833d95b49c9de17d8c0bde288 Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Mon, 23 Aug 2021 16:41:45 -0400 Subject: [PATCH 06/12] pubsub: improve handling of closed blocking subsciptions. (#6852) --- libs/pubsub/pubsub.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/pubsub/pubsub.go b/libs/pubsub/pubsub.go index 7548470b5..68d1ec941 100644 --- a/libs/pubsub/pubsub.go +++ b/libs/pubsub/pubsub.go @@ -507,7 +507,10 @@ func (state *state) send(msg interface{}, events []types.Event) error { for clientID, subscription := range clientSubscriptions { if cap(subscription.out) == 0 { // block on unbuffered channel - subscription.out <- NewMessage(subscription.id, msg, events) + select { + case subscription.out <- NewMessage(subscription.id, msg, events): + case <-subscription.canceled: + } } else { // don't block on buffered channels select { From ce3c059a0d40a1485b9aa2c87707b4f4fc8104f0 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Mon, 23 Aug 2021 15:06:01 -0700 Subject: [PATCH 07/12] ADR 072: Re-instate a request-for-comments archive. (#6851) This ADR restores a variation of the old Request for Comments documentation that we previously used. The proposal differs from the original formulation, and does not replace ADRs. --- docs/architecture/README.md | 2 + .../adr-072-request-for-comments.md | 105 ++++++++++++++++++ docs/rfc/README.md | 40 +++++++ docs/rfc/rfc-template.md | 35 ++++++ 4 files changed, 182 insertions(+) create mode 100644 docs/architecture/adr-072-request-for-comments.md create mode 100644 docs/rfc/README.md create mode 100644 docs/rfc/rfc-template.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 7025a72f6..f6c12996f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -98,3 +98,5 @@ Note the context/background should be written in the present tense. - [ADR-045: ABCI-Evidence](./adr-045-abci-evidence.md) - [ADR-057: RPC](./adr-057-RPC.md) - [ADR-069: Node Initialization](./adr-069-flexible-node-initialization.md) +- [ADR-071: Proposer-Based Timestamps](adr-071-proposer-based-timestamps.md) +- [ADR-072: Restore Requests for Comments](./adr-072-request-for-comments.md) diff --git a/docs/architecture/adr-072-request-for-comments.md b/docs/architecture/adr-072-request-for-comments.md new file mode 100644 index 000000000..7eb22ebc9 --- /dev/null +++ b/docs/architecture/adr-072-request-for-comments.md @@ -0,0 +1,105 @@ +# ADR 72: Restore Requests for Comments + +## Changelog + +- 20-Aug-2021: Initial draft (@creachadair) + +## Status + +Proposed + +## Context + +In the past, we kept a collection of Request for Comments (RFC) documents in `docs/rfc`. +Prior to the creation of the ADR process, these documents were used to document +design and implementation decisions about Tendermint Core. The RFC directory +was removed in favor of ADRs, in commit 3761aa69 (PR +[\#6345](https://github.com/tendermint/tendermint/pull/6345)). + +For issues where an explicit design decision or implementation change is +required, an ADR is generally preferable to an open-ended RFC: An ADR is +relatively narrowly-focused, identifies a specific design or implementation +question, and documents the consensus answer to that question. + +Some discussions are more open-ended, however, or don't require a specific +decision to be made (yet). Such conversations are still valuable to document, +and several members of the Tendermint team have been doing so by writing gists +or Google docs to share them around. That works well enough in the moment, but +gists do not support any kind of collaborative editing, and both gists and docs +are hard to discover after the fact. Google docs have much better collaborative +editing, but are worse for discoverability, especially when contributors span +different Google accounts. + +Discoverability is important, because these kinds of open-ended discussions are +useful to people who come later -- either as new team members or as outside +contributors seeking to use and understand the thoughts behind our designs and +the architectural decisions that arose from those discussion. + +With these in mind, I propose that: + +- We re-create a new, initially empty `docs/rfc` directory in the repository, + and use it to capture these kinds of open-ended discussions in supplement to + ADRs. + +- Unlike in the previous RFC scheme, documents in this new directory will + _not_ be used directly for decision-making. This is the key difference + between an RFC and an ADR. + + Instead, an RFC will exist to document background, articulate general + principles, and serve as a historical record of discussion and motivation. + + In this system, an RFC may _only_ result in a decision indirectly, via ADR + documents created in response to the RFC. + + **In short:** If a decision is required, write an ADR; otherwise if a + sufficiently broad discussion is needed, write an RFC. + +Just so that there is a consistent format, I also propose that: + +- RFC files are named `rfc-XXX-title.{md,rst,txt}` and are written in plain + text, Markdown, or ReStructured Text. + +- Like an ADR, an RFC should include a high-level change log at the top of the + document, and sections for: + + * Abstract: A brief, high-level synopsis of the topic. + * Background: Any background necessary to understand the topic. + * Discussion: Detailed discussion of the issue being considered. + +- Unlike an ADR, an RFC does _not_ include sections for Decisions, Detailed + Design, or evaluation of proposed solutions. If an RFC leads to a proposal + for an actual architectural change, that must be recorded in an ADR in the + usual way, and may refer back to the RFC in its References section. + +## Alternative Approaches + +Leaving aside implementation details, the main alternative to this proposal is +to leave things as they are now, with ADRs as the only log of record and other +discussions being held informally in whatever medium is convenient at the time. + +## Decision + +(pending) + +## Detailed Design + +- Create a new `docs/rfc` directory in the `tendermint` repository. Note that + this proposal intentionally does _not_ pull back the previous contents of + that path from Git history, as those documents were appropriately merged into + the ADR process. + +- Create a `README.md` for RFCs that explains the rules and their relationship + to ADRs. + +- Create an `rfc-template.md` file for RFC files. + +## Consequences + +### Positive + +- We will have a more discoverable place to record open-ended discussions that + do not immediately result in a design change. + +### Negative + +- Potentially some people could be confused about the RFC/ADR distinction. diff --git a/docs/rfc/README.md b/docs/rfc/README.md new file mode 100644 index 000000000..c05853aca --- /dev/null +++ b/docs/rfc/README.md @@ -0,0 +1,40 @@ +--- +order: 1 +parent: + order: false +--- + +# Requests for Comments + +A Request for Comments (RFC) is a record of discussion on an open-ended topic +related to the design and implementation of Tendermint Core, for which no +immediate decision is required. + +The purpose of an RFC is to serve as a historical record of a high-level +discussion that might otherwise only be recorded in an ad hoc way (for example, +via gists or Google docs) that are difficult to discover for someone after the +fact. An RFC _may_ give rise to more specific architectural _decisions_ for +Tendermint, but those decisions must be recorded separately in [Architecture +Decision Records (ADR)](./../architecture). + +As a rule of thumb, if you can articulate a specific question that needs to be +answered, write an ADR. If you need to explore the topic and get input from +others to know what questions need to be answered, an RFC may be appropriate. + +## RFC Content + +An RFC should provide: + +- A **changelog**, documenting when and how the RFC has changed. +- An **abstract**, briefly summarizing the topic so the reader can quickly tell + whether it is relevant to their interest. +- Any **background** a reader will need to understand and participate in the + substance of the discussion (links to other documents are fine here). +- The **discussion**, the primary content of the document. + +The [rfc-template.md](./rfc-template.md) file includes placeholders for these +sections. + +## Table of Contents + + diff --git a/docs/rfc/rfc-template.md b/docs/rfc/rfc-template.md new file mode 100644 index 000000000..b3f404775 --- /dev/null +++ b/docs/rfc/rfc-template.md @@ -0,0 +1,35 @@ +# RFC {RFC-NUMBER}: {TITLE} + +## Changelog + +- {date}: {changelog} + +## Abstract + +> A brief high-level synopsis of the topic of discussion for this RFC, ideally +> just a few sentences. This should help the reader quickly decide whether the +> rest of the discussion is relevant to their interest. + +## Background + +> Any context or orientation needed for a reader to understand and participate +> in the substance of the Discussion. If necessary, this section may include +> links to other documentation or sources rather than restating existing +> material, but should provide enough detail that the reader can tell what they +> need to read to be up-to-date. + +### References + +> Links to external materials needed to follow the discussion may be added here. +> +> In addition, if the discussion in a request for comments leads to any design +> decisions, it may be helpful to add links to the ADR documents here after the +> discussion has settled. + +## Discussion + +> This section contains the core of the discussion. +> +> There is no fixed format for this section, but ideally changes to this +> section should be updated before merging to reflect any discussion that took +> place on the PR that made those changes. From d7c3a8f682d2acdcdce859d86e732d33db1c9d1e Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Tue, 24 Aug 2021 11:43:13 -0400 Subject: [PATCH 08/12] time: make median time library type private (#6853) This is a very minor change, but I was looking through the code, and this seems like it shouldn't be exported or used more broadly, so I've moved it out. --- libs/time/time.go | 41 --------------------------- state/state.go | 7 ++--- state/time.go | 46 +++++++++++++++++++++++++++++++ {libs/time => state}/time_test.go | 39 +++++++++++++------------- 4 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 state/time.go rename {libs/time => state}/time_test.go (50%) diff --git a/libs/time/time.go b/libs/time/time.go index 022bdf574..786f9bbb4 100644 --- a/libs/time/time.go +++ b/libs/time/time.go @@ -1,7 +1,6 @@ package time import ( - "sort" "time" ) @@ -16,43 +15,3 @@ func Now() time.Time { func Canonical(t time.Time) time.Time { return t.Round(0).UTC() } - -// WeightedTime for computing a median. -type WeightedTime struct { - Time time.Time - Weight int64 -} - -// NewWeightedTime with time and weight. -func NewWeightedTime(time time.Time, weight int64) *WeightedTime { - return &WeightedTime{ - Time: time, - Weight: weight, - } -} - -// WeightedMedian computes weighted median time for a given array of WeightedTime and the total voting power. -func WeightedMedian(weightedTimes []*WeightedTime, totalVotingPower int64) (res time.Time) { - median := totalVotingPower / 2 - - sort.Slice(weightedTimes, func(i, j int) bool { - if weightedTimes[i] == nil { - return false - } - if weightedTimes[j] == nil { - return true - } - return weightedTimes[i].Time.UnixNano() < weightedTimes[j].Time.UnixNano() - }) - - for _, weightedTime := range weightedTimes { - if weightedTime != nil { - if median <= weightedTime.Weight { - res = weightedTime.Time - break - } - median -= weightedTime.Weight - } - } - return -} diff --git a/state/state.go b/state/state.go index 132a86fda..5862162d1 100644 --- a/state/state.go +++ b/state/state.go @@ -9,7 +9,6 @@ import ( "github.com/gogo/protobuf/proto" - tmtime "github.com/tendermint/tendermint/libs/time" tmstate "github.com/tendermint/tendermint/proto/tendermint/state" tmversion "github.com/tendermint/tendermint/proto/tendermint/version" "github.com/tendermint/tendermint/types" @@ -287,7 +286,7 @@ func (state State) MakeBlock( // the votes sent by honest processes, i.e., a faulty processes can not arbitrarily increase or decrease the // computed value. func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time { - weightedTimes := make([]*tmtime.WeightedTime, len(commit.Signatures)) + weightedTimes := make([]*weightedTime, len(commit.Signatures)) totalVotingPower := int64(0) for i, commitSig := range commit.Signatures { @@ -298,11 +297,11 @@ func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time // If there's no condition, TestValidateBlockCommit panics; not needed normally. if validator != nil { totalVotingPower += validator.VotingPower - weightedTimes[i] = tmtime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower) + weightedTimes[i] = newWeightedTime(commitSig.Timestamp, validator.VotingPower) } } - return tmtime.WeightedMedian(weightedTimes, totalVotingPower) + return weightedMedian(weightedTimes, totalVotingPower) } //------------------------------------------------------------------------ diff --git a/state/time.go b/state/time.go new file mode 100644 index 000000000..c0770b3af --- /dev/null +++ b/state/time.go @@ -0,0 +1,46 @@ +package state + +import ( + "sort" + "time" +) + +// weightedTime for computing a median. +type weightedTime struct { + Time time.Time + Weight int64 +} + +// newWeightedTime with time and weight. +func newWeightedTime(time time.Time, weight int64) *weightedTime { + return &weightedTime{ + Time: time, + Weight: weight, + } +} + +// weightedMedian computes weighted median time for a given array of WeightedTime and the total voting power. +func weightedMedian(weightedTimes []*weightedTime, totalVotingPower int64) (res time.Time) { + median := totalVotingPower / 2 + + sort.Slice(weightedTimes, func(i, j int) bool { + if weightedTimes[i] == nil { + return false + } + if weightedTimes[j] == nil { + return true + } + return weightedTimes[i].Time.UnixNano() < weightedTimes[j].Time.UnixNano() + }) + + for _, weightedTime := range weightedTimes { + if weightedTime != nil { + if median <= weightedTime.Weight { + res = weightedTime.Time + break + } + median -= weightedTime.Weight + } + } + return +} diff --git a/libs/time/time_test.go b/state/time_test.go similarity index 50% rename from libs/time/time_test.go rename to state/time_test.go index 1b1a30e50..893ade7ea 100644 --- a/libs/time/time_test.go +++ b/state/time_test.go @@ -1,54 +1,55 @@ -package time +package state import ( "testing" "time" "github.com/stretchr/testify/assert" + tmtime "github.com/tendermint/tendermint/libs/time" ) func TestWeightedMedian(t *testing.T) { - m := make([]*WeightedTime, 3) + m := make([]*weightedTime, 3) - t1 := Now() + t1 := tmtime.Now() t2 := t1.Add(5 * time.Second) t3 := t1.Add(10 * time.Second) - m[2] = NewWeightedTime(t1, 33) // faulty processes - m[0] = NewWeightedTime(t2, 40) // correct processes - m[1] = NewWeightedTime(t3, 27) // correct processes + m[2] = newWeightedTime(t1, 33) // faulty processes + m[0] = newWeightedTime(t2, 40) // correct processes + m[1] = newWeightedTime(t3, 27) // correct processes totalVotingPower := int64(100) - median := WeightedMedian(m, totalVotingPower) + median := weightedMedian(m, totalVotingPower) assert.Equal(t, t2, median) // median always returns value between values of correct processes assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) && (median.Before(t3) || median.Equal(t3))) - m[1] = NewWeightedTime(t1, 40) // correct processes - m[2] = NewWeightedTime(t2, 27) // correct processes - m[0] = NewWeightedTime(t3, 33) // faulty processes + m[1] = newWeightedTime(t1, 40) // correct processes + m[2] = newWeightedTime(t2, 27) // correct processes + m[0] = newWeightedTime(t3, 33) // faulty processes totalVotingPower = int64(100) - median = WeightedMedian(m, totalVotingPower) + median = weightedMedian(m, totalVotingPower) assert.Equal(t, t2, median) // median always returns value between values of correct processes assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) && (median.Before(t2) || median.Equal(t2))) - m = make([]*WeightedTime, 8) + m = make([]*weightedTime, 8) t4 := t1.Add(15 * time.Second) t5 := t1.Add(60 * time.Second) - m[3] = NewWeightedTime(t1, 10) // correct processes - m[1] = NewWeightedTime(t2, 10) // correct processes - m[5] = NewWeightedTime(t2, 10) // correct processes - m[4] = NewWeightedTime(t3, 23) // faulty processes - m[0] = NewWeightedTime(t4, 20) // correct processes - m[7] = NewWeightedTime(t5, 10) // faulty processes + m[3] = newWeightedTime(t1, 10) // correct processes + m[1] = newWeightedTime(t2, 10) // correct processes + m[5] = newWeightedTime(t2, 10) // correct processes + m[4] = newWeightedTime(t3, 23) // faulty processes + m[0] = newWeightedTime(t4, 20) // correct processes + m[7] = newWeightedTime(t5, 10) // faulty processes totalVotingPower = int64(83) - median = WeightedMedian(m, totalVotingPower) + median = weightedMedian(m, totalVotingPower) assert.Equal(t, t3, median) // median always returns value between values of correct processes assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) && From d8642a941e3602d1f7c89d29c9d3e844e14e5919 Mon Sep 17 00:00:00 2001 From: Sam Kleinman Date: Tue, 24 Aug 2021 12:06:27 -0400 Subject: [PATCH 09/12] cmd: remove deprecated snakes (#6854) just following up on a deprecation. --- CHANGELOG_PENDING.md | 4 ++-- cmd/tendermint/commands/gen_node_key.go | 8 +++----- cmd/tendermint/commands/gen_validator.go | 8 +++----- cmd/tendermint/commands/probe_upnp.go | 8 +++----- cmd/tendermint/commands/replay.go | 6 ++---- cmd/tendermint/commands/reset_priv_validator.go | 16 ++++++---------- cmd/tendermint/commands/root.go | 8 -------- cmd/tendermint/commands/show_node_id.go | 8 +++----- cmd/tendermint/commands/show_validator.go | 8 +++----- 9 files changed, 25 insertions(+), 49 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index bb5221bb5..233d11887 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -25,7 +25,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi - [rpc/grpc] \#6725 Mark gRPC in the RPC layer as deprecated. - [blockchain/v2] \#6730 Fast Sync v2 is deprecated, please use v0 - [rpc] \#6820 Update RPC methods to reflect changes in the p2p layer, disabling support for `UnsafeDialPeers` and `UnsafeDialPeers` when used with the new p2p layer, and changing the response format of the peer list in `NetInfo` for all users. - + - [cli] \#6854 Remove deprecated snake case commands. (@tychoish) - Apps - [ABCI] \#6408 Change the `key` and `value` fields from `[]byte` to `string` in the `EventAttribute` type. (@alexanderbez) - [ABCI] \#5447 Remove `SetOption` method from `ABCI.Client` interface @@ -158,4 +158,4 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi - [rpc] \#6507 Ensure RPC client can handle URLs without ports (@JayT106) - [statesync] \#6463 Adds Reverse Sync feature to fetch historical light blocks after state sync in order to verify any evidence (@cmwaters) - [fastsync] \#6590 Update the metrics during fast-sync (@JayT106) -- [gitignore] \#6668 Fix gitignore of abci-cli (@tanyabouman) \ No newline at end of file +- [gitignore] \#6668 Fix gitignore of abci-cli (@tanyabouman) diff --git a/cmd/tendermint/commands/gen_node_key.go b/cmd/tendermint/commands/gen_node_key.go index f796f4b7f..d8b493e3c 100644 --- a/cmd/tendermint/commands/gen_node_key.go +++ b/cmd/tendermint/commands/gen_node_key.go @@ -12,11 +12,9 @@ import ( // GenNodeKeyCmd allows the generation of a node key. It prints JSON-encoded // NodeKey to the standard output. var GenNodeKeyCmd = &cobra.Command{ - Use: "gen-node-key", - Aliases: []string{"gen_node_key"}, - Short: "Generate a new node key", - RunE: genNodeKey, - PreRun: deprecateSnakeCase, + Use: "gen-node-key", + Short: "Generate a new node key", + RunE: genNodeKey, } func genNodeKey(cmd *cobra.Command, args []string) error { diff --git a/cmd/tendermint/commands/gen_validator.go b/cmd/tendermint/commands/gen_validator.go index 09f84b09e..830518ce9 100644 --- a/cmd/tendermint/commands/gen_validator.go +++ b/cmd/tendermint/commands/gen_validator.go @@ -13,11 +13,9 @@ import ( // GenValidatorCmd allows the generation of a keypair for a // validator. var GenValidatorCmd = &cobra.Command{ - Use: "gen-validator", - Aliases: []string{"gen_validator"}, - Short: "Generate new validator keypair", - RunE: genValidator, - PreRun: deprecateSnakeCase, + Use: "gen-validator", + Short: "Generate new validator keypair", + RunE: genValidator, } func init() { diff --git a/cmd/tendermint/commands/probe_upnp.go b/cmd/tendermint/commands/probe_upnp.go index 4471024f9..4c71e099a 100644 --- a/cmd/tendermint/commands/probe_upnp.go +++ b/cmd/tendermint/commands/probe_upnp.go @@ -11,11 +11,9 @@ import ( // ProbeUpnpCmd adds capabilities to test the UPnP functionality. var ProbeUpnpCmd = &cobra.Command{ - Use: "probe-upnp", - Aliases: []string{"probe_upnp"}, - Short: "Test UPnP functionality", - RunE: probeUpnp, - PreRun: deprecateSnakeCase, + Use: "probe-upnp", + Short: "Test UPnP functionality", + RunE: probeUpnp, } func probeUpnp(cmd *cobra.Command, args []string) error { diff --git a/cmd/tendermint/commands/replay.go b/cmd/tendermint/commands/replay.go index 6e736bca2..e92274042 100644 --- a/cmd/tendermint/commands/replay.go +++ b/cmd/tendermint/commands/replay.go @@ -17,11 +17,9 @@ var ReplayCmd = &cobra.Command{ // ReplayConsoleCmd allows replaying of messages from the WAL in a // console. var ReplayConsoleCmd = &cobra.Command{ - Use: "replay-console", - Aliases: []string{"replay_console"}, - Short: "Replay messages from WAL in a console", + Use: "replay-console", + Short: "Replay messages from WAL in a console", Run: func(cmd *cobra.Command, args []string) { consensus.RunReplayFile(config.BaseConfig, config.Consensus, true) }, - PreRun: deprecateSnakeCase, } diff --git a/cmd/tendermint/commands/reset_priv_validator.go b/cmd/tendermint/commands/reset_priv_validator.go index 046780ef1..8745e55d8 100644 --- a/cmd/tendermint/commands/reset_priv_validator.go +++ b/cmd/tendermint/commands/reset_priv_validator.go @@ -14,11 +14,9 @@ import ( // ResetAllCmd removes the database of this Tendermint core // instance. var ResetAllCmd = &cobra.Command{ - Use: "unsafe-reset-all", - Aliases: []string{"unsafe_reset_all"}, - Short: "(unsafe) Remove all the data and WAL, reset this node's validator to genesis state", - RunE: resetAll, - PreRun: deprecateSnakeCase, + Use: "unsafe-reset-all", + Short: "(unsafe) Remove all the data and WAL, reset this node's validator to genesis state", + RunE: resetAll, } var keepAddrBook bool @@ -31,11 +29,9 @@ func init() { // ResetPrivValidatorCmd resets the private validator files. var ResetPrivValidatorCmd = &cobra.Command{ - Use: "unsafe-reset-priv-validator", - Aliases: []string{"unsafe_reset_priv_validator"}, - Short: "(unsafe) Reset this node's validator to genesis state", - RunE: resetPrivValidator, - PreRun: deprecateSnakeCase, + Use: "unsafe-reset-priv-validator", + Short: "(unsafe) Reset this node's validator to genesis state", + RunE: resetPrivValidator, } // XXX: this is totally unsafe. diff --git a/cmd/tendermint/commands/root.go b/cmd/tendermint/commands/root.go index 02f260de5..2289ae363 100644 --- a/cmd/tendermint/commands/root.go +++ b/cmd/tendermint/commands/root.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "strings" "time" "github.com/spf13/cobra" @@ -65,10 +64,3 @@ var RootCmd = &cobra.Command{ return nil }, } - -// deprecateSnakeCase is a util function for 0.34.1. Should be removed in 0.35 -func deprecateSnakeCase(cmd *cobra.Command, args []string) { - if strings.Contains(cmd.CalledAs(), "_") { - fmt.Println("Deprecated: snake_case commands will be replaced by hyphen-case commands in the next major release") - } -} diff --git a/cmd/tendermint/commands/show_node_id.go b/cmd/tendermint/commands/show_node_id.go index 7a5814c3b..488f4c322 100644 --- a/cmd/tendermint/commands/show_node_id.go +++ b/cmd/tendermint/commands/show_node_id.go @@ -8,11 +8,9 @@ import ( // ShowNodeIDCmd dumps node's ID to the standard output. var ShowNodeIDCmd = &cobra.Command{ - Use: "show-node-id", - Aliases: []string{"show_node_id"}, - Short: "Show this node's ID", - RunE: showNodeID, - PreRun: deprecateSnakeCase, + Use: "show-node-id", + Short: "Show this node's ID", + RunE: showNodeID, } func showNodeID(cmd *cobra.Command, args []string) error { diff --git a/cmd/tendermint/commands/show_validator.go b/cmd/tendermint/commands/show_validator.go index 240ed943f..47b372c61 100644 --- a/cmd/tendermint/commands/show_validator.go +++ b/cmd/tendermint/commands/show_validator.go @@ -16,11 +16,9 @@ import ( // ShowValidatorCmd adds capabilities for showing the validator info. var ShowValidatorCmd = &cobra.Command{ - Use: "show-validator", - Aliases: []string{"show_validator"}, - Short: "Show this node's validator info", - RunE: showValidator, - PreRun: deprecateSnakeCase, + Use: "show-validator", + Short: "Show this node's validator info", + RunE: showValidator, } func showValidator(cmd *cobra.Command, args []string) error { From 6d5ff590c384fcc9d1f32f4adef1c67670821394 Mon Sep 17 00:00:00 2001 From: Tess Rinearson Date: Tue, 24 Aug 2021 19:07:53 +0200 Subject: [PATCH 10/12] contributing: remove release_notes.md reference (#6846) --- .github/workflows/release.yml | 7 ++----- CONTRIBUTING.md | 2 -- release_notes.md | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 release_notes.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 771b8ab7c..dd18e750b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: "Release" on: push: - branches: + branches: - "RC[0-9]/**" tags: - "v[0-9]+.[0-9]+.[0-9]+" # Push events to matching v*, i.e. v1.0, v20.15.10 @@ -20,9 +20,6 @@ jobs: with: go-version: '1.16' - - run: echo https://github.com/tendermint/tendermint/blob/${GITHUB_REF#refs/tags/}/CHANGELOG.md#${GITHUB_REF#refs/tags/} > ../release_notes.md - if: startsWith(github.ref, 'refs/tags/') - - name: Build uses: goreleaser/goreleaser-action@v2 if: ${{ github.event_name == 'pull_request' }} @@ -35,6 +32,6 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: version: latest - args: release --rm-dist --release-notes=../release_notes.md + args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79729cc1b..23bfafcdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -328,7 +328,6 @@ If there were no release candidates, begin by creating a backport branch, as des - Bump TMVersionDefault version in `version.go` - Bump P2P and block protocol versions in `version.go`, if necessary - Bump ABCI protocol version in `version.go`, if necessary - - Add any release notes you would like to be added to the body of the release to `release_notes.md`. 4. Open a PR with these changes against the backport branch. 5. Once these changes are on the backport branch, push a tag with prepared release details. This will trigger the actual release `v0.35.0`. @@ -355,7 +354,6 @@ To create a minor release: - 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.) - - Add any release notes you would like to be added to the body of the release to `release_notes.md`. 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'` diff --git a/release_notes.md b/release_notes.md deleted file mode 100644 index a537871c5..000000000 --- a/release_notes.md +++ /dev/null @@ -1 +0,0 @@ - From bc2b529b95fd74b05e7b966e007b31fee186867e Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Tue, 24 Aug 2021 14:12:06 -0400 Subject: [PATCH 11/12] inspect: add inspect mode for debugging crashed tendermint node (#6785) EDIT: Updated, see [comment below]( https://github.com/tendermint/tendermint/pull/6785#issuecomment-897793175) This change adds a sketch of the `Debug` mode. This change adds a `Debug` struct to the node package. This `Debug` struct is intended to be created and started by a command in the `cmd` directory. The `Debug` struct runs the RPC server on the data directories: both the state store and the block store. This change required a good deal of refactoring. Namely, a new `rpc.go` file was added to the `node` package. This file encapsulates functions for starting RPC servers used by nodes. A potential additional change is to further factor this code into shared code _in_ the `rpc` package. Minor API tweaks were also made that seemed appropriate such as the mechanism for fetching routes from the `rpc/core` package. Additional work is required to register the `Debug` service as a command in the `cmd` directory but I am looking for feedback on if this direction seems appropriate before diving much further. closes: #5908 --- CHANGELOG_PENDING.md | 3 + cmd/tendermint/commands/inspect.go | 87 ++++ cmd/tendermint/main.go | 1 + docs/tools/debugging/README.md | 27 ++ docs/tools/debugging/pro.md | 35 +- go.mod | 1 + go.sum | 1 + inspect/doc.go | 36 ++ inspect/inspect.go | 149 +++++++ inspect/inspect_test.go | 583 +++++++++++++++++++++++++ inspect/rpc/rpc.go | 143 ++++++ light/proxy/proxy.go | 2 +- node/node.go | 27 +- node/setup.go | 58 +-- rpc/core/env.go | 17 +- rpc/jsonrpc/jsonrpc_test.go | 4 +- rpc/jsonrpc/server/http_server.go | 6 +- rpc/jsonrpc/server/http_server_test.go | 3 +- rpc/jsonrpc/test/main.go | 2 +- state/indexer/indexer_service.go | 66 +-- state/indexer/sink/sink.go | 65 +++ 21 files changed, 1200 insertions(+), 116 deletions(-) create mode 100644 cmd/tendermint/commands/inspect.go create mode 100644 inspect/doc.go create mode 100644 inspect/inspect.go create mode 100644 inspect/inspect_test.go create mode 100644 inspect/rpc/rpc.go create mode 100644 state/indexer/sink/sink.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 233d11887..6c25ef89e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -24,6 +24,8 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi - [fastsync/rpc] \#6620 Add TotalSyncedTime & RemainingTime to SyncInfo in /status RPC (@JayT106) - [rpc/grpc] \#6725 Mark gRPC in the RPC layer as deprecated. - [blockchain/v2] \#6730 Fast Sync v2 is deprecated, please use v0 + - [rpc] Add genesis_chunked method to support paginated and parallel fetching of large genesis documents. + - [rpc/jsonrpc/server] \#6785 `Listen` function updated to take an `int` argument, `maxOpenConnections`, instead of an entire config object. (@williambanfield) - [rpc] \#6820 Update RPC methods to reflect changes in the p2p layer, disabling support for `UnsafeDialPeers` and `UnsafeDialPeers` when used with the new p2p layer, and changing the response format of the peer list in `NetInfo` for all users. - [cli] \#6854 Remove deprecated snake case commands. (@tychoish) - Apps @@ -105,6 +107,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi - [config/indexer] \#6411 Introduce support for custom event indexing data sources, specifically PostgreSQL. (@JayT106) - [fastsync/event] \#6619 Emit fastsync status event when switching consensus/fastsync (@JayT106) - [statesync/event] \#6700 Emit statesync status start/end event (@JayT106) +- [inspect] \#6785 Add a new `inspect` command for introspecting the state and block store of a crashed tendermint node. (@williambanfield) ### IMPROVEMENTS diff --git a/cmd/tendermint/commands/inspect.go b/cmd/tendermint/commands/inspect.go new file mode 100644 index 000000000..de31d33d4 --- /dev/null +++ b/cmd/tendermint/commands/inspect.go @@ -0,0 +1,87 @@ +package commands + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/inspect" + "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/state/indexer/sink" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" +) + +// InspectCmd is the command for starting an inspect server. +var InspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Run an inspect server for investigating Tendermint state", + Long: ` + inspect runs a subset of Tendermint's RPC endpoints that are useful for debugging + issues with Tendermint. + + When the Tendermint consensus engine detects inconsistent state, it will crash the + tendermint process. Tendermint will not start up while in this inconsistent state. + The inspect command can be used to query the block and state store using Tendermint + RPC calls to debug issues of inconsistent state. + `, + + RunE: runInspect, +} + +func init() { + InspectCmd.Flags(). + String("rpc.laddr", + config.RPC.ListenAddress, "RPC listenener address. Port required") + InspectCmd.Flags(). + String("db-backend", + config.DBBackend, "database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb") + InspectCmd.Flags(). + String("db-dir", config.DBPath, "database directory") +} + +func runInspect(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-c + cancel() + }() + + blockStoreDB, err := cfg.DefaultDBProvider(&cfg.DBContext{ID: "blockstore", Config: config}) + if err != nil { + return err + } + blockStore := store.NewBlockStore(blockStoreDB) + stateDB, err := cfg.DefaultDBProvider(&cfg.DBContext{ID: "state", Config: config}) + if err != nil { + if err := blockStoreDB.Close(); err != nil { + logger.Error("error closing block store db", "error", err) + } + return err + } + genDoc, err := types.GenesisDocFromFile(config.GenesisFile()) + if err != nil { + return err + } + sinks, err := sink.EventSinksFromConfig(config, cfg.DefaultDBProvider, genDoc.ChainID) + if err != nil { + return err + } + stateStore := state.NewStore(stateDB) + + ins := inspect.New(config.RPC, blockStore, stateStore, sinks, logger) + + logger.Info("starting inspect server") + if err := ins.Run(ctx); err != nil { + return err + } + return nil +} diff --git a/cmd/tendermint/main.go b/cmd/tendermint/main.go index b092f5645..c006c297d 100644 --- a/cmd/tendermint/main.go +++ b/cmd/tendermint/main.go @@ -28,6 +28,7 @@ func main() { cmd.ShowNodeIDCmd, cmd.GenNodeKeyCmd, cmd.VersionCmd, + cmd.InspectCmd, cmd.MakeKeyMigrateCommand(), debug.DebugCmd, cli.NewCompletionCmd(rootCmd, true), diff --git a/docs/tools/debugging/README.md b/docs/tools/debugging/README.md index 2932f6e86..053b43624 100644 --- a/docs/tools/debugging/README.md +++ b/docs/tools/debugging/README.md @@ -62,3 +62,30 @@ given destination directory. Each archive will contain: Note: goroutine.out and heap.out will only be written if a profile address is provided and is operational. This command is blocking and will log any error. + +## Tendermint Inspect + +Tendermint includes an `inspect` command for querying Tendermint's state store and block +store over Tendermint RPC. + +When the Tendermint consensus engine detects inconsistent state, it will crash the +entire Tendermint process. +While in this inconsistent state, a node running Tendermint's consensus engine will not start up. +The `inspect` command runs only a subset of Tendermint's RPC endpoints for querying the block store +and state store. +`inspect` allows operators to query a read-only view of the stage. +`inspect` does not run the consensus engine at all and can therefore be used to debug +processes that have crashed due to inconsistent state. + + +To start the `inspect` process, run +```bash +tendermint inspect +``` + +### RPC endpoints +The list of available RPC endpoints can be found by making a request to the RPC port. +For an `inspect` process running on `127.0.0.1:26657`, navigate your browser to +`http://127.0.0.1:26657/` to retrieve the list of enabled RPC endpoints. + +Additional information on the Tendermint RPC endpoints can be found in the [rpc documentation](https://docs.tendermint.com/master/rpc). diff --git a/docs/tools/debugging/pro.md b/docs/tools/debugging/pro.md index 3342deb49..b43ed5cba 100644 --- a/docs/tools/debugging/pro.md +++ b/docs/tools/debugging/pro.md @@ -64,13 +64,42 @@ It won’t kill the node, but it will gather all of the above data and package i At this point, depending on how severe the degradation is, you may want to restart the process. +## Tendermint Inspect + +What if the Tendermint node will not start up due to inconsistent consensus state? + +When a node running the Tendermint consensus engine detects an inconsistent state +it will crash the entire Tendermint process. +The Tendermint consensus engine cannot be run in this inconsistent state and the so node +will fail to start up as a result. +The Tendermint RPC server can provide valuable information for debugging in this situation. +The Tendermint `inspect` command will run a subset of the Tendermint RPC server +that is useful for debugging inconsistent state. + +### Running inspect + +Start up the `inspect` tool on the machine where Tendermint crashed using: +```bash +tendermint inspect --home= +``` + +`inspect` will use the data directory specified in your Tendermint configuration file. +`inspect` will also run the RPC server at the address specified in your Tendermint configuration file. + +### Using inspect + +With the `inspect` server running, you can access RPC endpoints that are critically important +for debugging. +Calling the `/status`, `/consensus_state` and `/dump_consensus_state` RPC endpoint +will return useful information about the Tendermint consensus state. + ## Outro -We’re hoping that the `tendermint debug` subcommand will become de facto the first response to any accidents. +We’re hoping that these Tendermint tools will become de facto the first response for any accidents. -Let us know what your experience has been so far! Have you had a chance to try `tendermint debug` yet? +Let us know what your experience has been so far! Have you had a chance to try `tendermint debug` or `tendermint inspect` yet? -Join our chat, where we discuss the current issues and future improvements. +Join our [discord chat](https://discord.gg/vcExX9T), where we discuss the current issues and future improvements. — diff --git a/go.mod b/go.mod index 35272bea1..c7723b096 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/vektra/mockery/v2 v2.9.0 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect google.golang.org/grpc v1.40.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect pgregory.net/rapid v0.4.7 diff --git a/go.sum b/go.sum index 74f189d5c..fff11dad3 100644 --- a/go.sum +++ b/go.sum @@ -1075,6 +1075,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/inspect/doc.go b/inspect/doc.go new file mode 100644 index 000000000..c53049e1a --- /dev/null +++ b/inspect/doc.go @@ -0,0 +1,36 @@ +/* +Package inspect provides a tool for investigating the state of a +failed Tendermint node. + +This package provides the Inspector type. The Inspector type runs a subset of the Tendermint +RPC endpoints that are useful for debugging issues with Tendermint consensus. + +When a node running the Tendermint consensus engine detects an inconsistent consensus state, +the entire node will crash. The Tendermint consensus engine cannot run in this +inconsistent state so the node will not be able to start up again. + +The RPC endpoints provided by the Inspector type allow for a node operator to inspect +the block store and state store to better understand what may have caused the inconsistent state. + + +The Inspector type's lifecycle is controlled by a context.Context + ins := inspect.NewFromConfig(rpcConfig) + ctx, cancelFunc:= context.WithCancel(context.Background()) + + // Run blocks until the Inspector server is shut down. + go ins.Run(ctx) + ... + + // calling the cancel function will stop the running inspect server + cancelFunc() + +Inspector serves its RPC endpoints on the address configured in the RPC configuration + + rpcConfig.ListenAddress = "tcp://127.0.0.1:26657" + ins := inspect.NewFromConfig(rpcConfig) + go ins.Run(ctx) + +The list of available RPC endpoints can then be viewed by navigating to +http://127.0.0.1:26657/ in the web browser. +*/ +package inspect diff --git a/inspect/inspect.go b/inspect/inspect.go new file mode 100644 index 000000000..38bc9ed5d --- /dev/null +++ b/inspect/inspect.go @@ -0,0 +1,149 @@ +package inspect + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/inspect/rpc" + "github.com/tendermint/tendermint/libs/log" + tmstrings "github.com/tendermint/tendermint/libs/strings" + rpccore "github.com/tendermint/tendermint/rpc/core" + "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/state/indexer" + "github.com/tendermint/tendermint/state/indexer/sink" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" + + "golang.org/x/sync/errgroup" +) + +// Inspector manages an RPC service that exports methods to debug a failed node. +// After a node shuts down due to a consensus failure, it will no longer start +// up its state cannot easily be inspected. An Inspector value provides a similar interface +// to the node, using the underlying Tendermint data stores, without bringing up +// any other components. A caller can query the Inspector service to inspect the +// persisted state and debug the failure. +type Inspector struct { + routes rpccore.RoutesMap + + config *config.RPCConfig + + indexerService *indexer.Service + eventBus *types.EventBus + logger log.Logger +} + +// New returns an Inspector that serves RPC on the specified BlockStore and StateStore. +// The Inspector type does not modify the state or block stores. +// The sinks are used to enable block and transaction querying via the RPC server. +// The caller is responsible for starting and stopping the Inspector service. +/// +//nolint:lll +func New(cfg *config.RPCConfig, bs state.BlockStore, ss state.Store, es []indexer.EventSink, logger log.Logger) *Inspector { + routes := rpc.Routes(*cfg, ss, bs, es, logger) + eb := types.NewEventBus() + eb.SetLogger(logger.With("module", "events")) + is := indexer.NewIndexerService(es, eb) + is.SetLogger(logger.With("module", "txindex")) + return &Inspector{ + routes: routes, + config: cfg, + logger: logger, + eventBus: eb, + indexerService: is, + } +} + +// NewFromConfig constructs an Inspector using the values defined in the passed in config. +func NewFromConfig(cfg *config.Config) (*Inspector, error) { + bsDB, err := config.DefaultDBProvider(&config.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return nil, err + } + bs := store.NewBlockStore(bsDB) + sDB, err := config.DefaultDBProvider(&config.DBContext{ID: "state", Config: cfg}) + if err != nil { + return nil, err + } + genDoc, err := types.GenesisDocFromFile(cfg.GenesisFile()) + if err != nil { + return nil, err + } + sinks, err := sink.EventSinksFromConfig(cfg, config.DefaultDBProvider, genDoc.ChainID) + if err != nil { + return nil, err + } + logger := log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false) + ss := state.NewStore(sDB) + return New(cfg.RPC, bs, ss, sinks, logger), nil +} + +// Run starts the Inspector servers and blocks until the servers shut down. The passed +// in context is used to control the lifecycle of the servers. +func (ins *Inspector) Run(ctx context.Context) error { + err := ins.eventBus.Start() + if err != nil { + return fmt.Errorf("error starting event bus: %s", err) + } + defer func() { + err := ins.eventBus.Stop() + if err != nil { + ins.logger.Error("event bus stopped with error", "err", err) + } + }() + err = ins.indexerService.Start() + if err != nil { + return fmt.Errorf("error starting indexer service: %s", err) + } + defer func() { + err := ins.indexerService.Stop() + if err != nil { + ins.logger.Error("indexer service stopped with error", "err", err) + } + }() + return startRPCServers(ctx, ins.config, ins.logger, ins.routes) +} + +func startRPCServers(ctx context.Context, cfg *config.RPCConfig, logger log.Logger, routes rpccore.RoutesMap) error { + g, tctx := errgroup.WithContext(ctx) + listenAddrs := tmstrings.SplitAndTrimEmpty(cfg.ListenAddress, ",", " ") + rh := rpc.Handler(cfg, routes, logger) + for _, listenerAddr := range listenAddrs { + server := rpc.Server{ + Logger: logger, + Config: cfg, + Handler: rh, + Addr: listenerAddr, + } + if cfg.IsTLSEnabled() { + keyFile := cfg.KeyFile() + certFile := cfg.CertFile() + listenerAddr := listenerAddr + g.Go(func() error { + logger.Info("RPC HTTPS server starting", "address", listenerAddr, + "certfile", certFile, "keyfile", keyFile) + err := server.ListenAndServeTLS(tctx, certFile, keyFile) + if !errors.Is(err, net.ErrClosed) { + return err + } + logger.Info("RPC HTTPS server stopped", "address", listenerAddr) + return nil + }) + } else { + listenerAddr := listenerAddr + g.Go(func() error { + logger.Info("RPC HTTP server starting", "address", listenerAddr) + err := server.ListenAndServe(tctx) + if !errors.Is(err, net.ErrClosed) { + return err + } + logger.Info("RPC HTTP server stopped", "address", listenerAddr) + return nil + }) + } + } + return g.Wait() +} diff --git a/inspect/inspect_test.go b/inspect/inspect_test.go new file mode 100644 index 000000000..c2a1df571 --- /dev/null +++ b/inspect/inspect_test.go @@ -0,0 +1,583 @@ +package inspect_test + +import ( + "context" + "fmt" + "net" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/fortytw2/leaktest" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + abcitypes "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/inspect" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/pubsub/query" + "github.com/tendermint/tendermint/proto/tendermint/state" + httpclient "github.com/tendermint/tendermint/rpc/client/http" + "github.com/tendermint/tendermint/state/indexer" + indexermocks "github.com/tendermint/tendermint/state/indexer/mocks" + statemocks "github.com/tendermint/tendermint/state/mocks" + "github.com/tendermint/tendermint/types" +) + +func TestInspectConstructor(t *testing.T) { + cfg := config.ResetTestRoot("test") + t.Cleanup(leaktest.Check(t)) + defer func() { _ = os.RemoveAll(cfg.RootDir) }() + t.Run("from config", func(t *testing.T) { + d, err := inspect.NewFromConfig(cfg) + require.NoError(t, err) + require.NotNil(t, d) + }) + +} + +func TestInspectRun(t *testing.T) { + cfg := config.ResetTestRoot("test") + t.Cleanup(leaktest.Check(t)) + defer func() { _ = os.RemoveAll(cfg.RootDir) }() + t.Run("from config", func(t *testing.T) { + d, err := inspect.NewFromConfig(cfg) + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + stoppedWG := &sync.WaitGroup{} + stoppedWG.Add(1) + go func() { + require.NoError(t, d.Run(ctx)) + stoppedWG.Done() + }() + cancel() + stoppedWG.Wait() + }) + +} + +func TestBlock(t *testing.T) { + testHeight := int64(1) + testBlock := new(types.Block) + testBlock.Header.Height = testHeight + testBlock.Header.LastCommitHash = []byte("test hash") + stateStoreMock := &statemocks.Store{} + + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Height").Return(testHeight) + blockStoreMock.On("Base").Return(int64(0)) + blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{}) + blockStoreMock.On("LoadBlock", testHeight).Return(testBlock) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + resultBlock, err := cli.Block(context.Background(), &testHeight) + require.NoError(t, err) + require.Equal(t, testBlock.Height, resultBlock.Block.Height) + require.Equal(t, testBlock.LastCommitHash, resultBlock.Block.LastCommitHash) + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestTxSearch(t *testing.T) { + testHash := []byte("test") + testTx := []byte("tx") + testQuery := fmt.Sprintf("tx.hash='%s'", string(testHash)) + testTxResult := &abcitypes.TxResult{ + Height: 1, + Index: 100, + Tx: testTx, + } + + stateStoreMock := &statemocks.Store{} + blockStoreMock := &statemocks.BlockStore{} + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + eventSinkMock.On("Type").Return(indexer.KV) + eventSinkMock.On("SearchTxEvents", mock.Anything, + mock.MatchedBy(func(q *query.Query) bool { return testQuery == q.String() })). + Return([]*abcitypes.TxResult{testTxResult}, nil) + + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + + var page = 1 + resultTxSearch, err := cli.TxSearch(context.Background(), testQuery, false, &page, &page, "") + require.NoError(t, err) + require.Len(t, resultTxSearch.Txs, 1) + require.Equal(t, types.Tx(testTx), resultTxSearch.Txs[0].Tx) + + cancel() + wg.Wait() + + eventSinkMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) + blockStoreMock.AssertExpectations(t) +} +func TestTx(t *testing.T) { + testHash := []byte("test") + testTx := []byte("tx") + + stateStoreMock := &statemocks.Store{} + blockStoreMock := &statemocks.BlockStore{} + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + eventSinkMock.On("Type").Return(indexer.KV) + eventSinkMock.On("GetTxByHash", testHash).Return(&abcitypes.TxResult{ + Tx: testTx, + }, nil) + + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + + res, err := cli.Tx(context.Background(), testHash, false) + require.NoError(t, err) + require.Equal(t, types.Tx(testTx), res.Tx) + + cancel() + wg.Wait() + + eventSinkMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) + blockStoreMock.AssertExpectations(t) +} +func TestConsensusParams(t *testing.T) { + testHeight := int64(1) + testMaxGas := int64(55) + stateStoreMock := &statemocks.Store{} + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Height").Return(testHeight) + blockStoreMock.On("Base").Return(int64(0)) + stateStoreMock.On("LoadConsensusParams", testHeight).Return(types.ConsensusParams{ + Block: types.BlockParams{ + MaxGas: testMaxGas, + }, + }, nil) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + params, err := cli.ConsensusParams(context.Background(), &testHeight) + require.NoError(t, err) + require.Equal(t, params.ConsensusParams.Block.MaxGas, testMaxGas) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestBlockResults(t *testing.T) { + testHeight := int64(1) + testGasUsed := int64(100) + stateStoreMock := &statemocks.Store{} + // tmstate "github.com/tendermint/tendermint/proto/tendermint/state" + stateStoreMock.On("LoadABCIResponses", testHeight).Return(&state.ABCIResponses{ + DeliverTxs: []*abcitypes.ResponseDeliverTx{ + { + GasUsed: testGasUsed, + }, + }, + EndBlock: &abcitypes.ResponseEndBlock{}, + BeginBlock: &abcitypes.ResponseBeginBlock{}, + }, nil) + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Base").Return(int64(0)) + blockStoreMock.On("Height").Return(testHeight) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + res, err := cli.BlockResults(context.Background(), &testHeight) + require.NoError(t, err) + require.Equal(t, res.TotalGasUsed, testGasUsed) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestCommit(t *testing.T) { + testHeight := int64(1) + testRound := int32(101) + stateStoreMock := &statemocks.Store{} + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Base").Return(int64(0)) + blockStoreMock.On("Height").Return(testHeight) + blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{}, nil) + blockStoreMock.On("LoadSeenCommit").Return(&types.Commit{ + Height: testHeight, + Round: testRound, + }, nil) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + res, err := cli.Commit(context.Background(), &testHeight) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, res.SignedHeader.Commit.Round, testRound) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestBlockByHash(t *testing.T) { + testHeight := int64(1) + testHash := []byte("test hash") + testBlock := new(types.Block) + testBlock.Header.Height = testHeight + testBlock.Header.LastCommitHash = testHash + stateStoreMock := &statemocks.Store{} + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ + BlockID: types.BlockID{ + Hash: testHash, + }, + Header: types.Header{ + Height: testHeight, + }, + }, nil) + blockStoreMock.On("LoadBlockByHash", testHash).Return(testBlock, nil) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + res, err := cli.BlockByHash(context.Background(), testHash) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, []byte(res.BlockID.Hash), testHash) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestBlockchain(t *testing.T) { + testHeight := int64(1) + testBlock := new(types.Block) + testBlockHash := []byte("test hash") + testBlock.Header.Height = testHeight + testBlock.Header.LastCommitHash = testBlockHash + stateStoreMock := &statemocks.Store{} + + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Height").Return(testHeight) + blockStoreMock.On("Base").Return(int64(0)) + blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ + BlockID: types.BlockID{ + Hash: testBlockHash, + }, + }) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + res, err := cli.BlockchainInfo(context.Background(), 0, 100) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, testBlockHash, []byte(res.BlockMetas[0].BlockID.Hash)) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestValidators(t *testing.T) { + testHeight := int64(1) + testVotingPower := int64(100) + testValidators := types.ValidatorSet{ + Validators: []*types.Validator{ + { + VotingPower: testVotingPower, + }, + }, + } + stateStoreMock := &statemocks.Store{} + stateStoreMock.On("LoadValidators", testHeight).Return(&testValidators, nil) + + blockStoreMock := &statemocks.BlockStore{} + blockStoreMock.On("Height").Return(testHeight) + blockStoreMock.On("Base").Return(int64(0)) + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + + testPage := 1 + testPerPage := 100 + res, err := cli.Validators(context.Background(), &testHeight, &testPage, &testPerPage) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, testVotingPower, res.Validators[0].VotingPower) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func TestBlockSearch(t *testing.T) { + testHeight := int64(1) + testBlockHash := []byte("test hash") + testQuery := "block.height = 1" + stateStoreMock := &statemocks.Store{} + + blockStoreMock := &statemocks.BlockStore{} + eventSinkMock := &indexermocks.EventSink{} + eventSinkMock.On("Stop").Return(nil) + eventSinkMock.On("Type").Return(indexer.KV) + blockStoreMock.On("LoadBlock", testHeight).Return(&types.Block{ + Header: types.Header{ + Height: testHeight, + }, + }, nil) + blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ + BlockID: types.BlockID{ + Hash: testBlockHash, + }, + }) + eventSinkMock.On("SearchBlockEvents", mock.Anything, + mock.MatchedBy(func(q *query.Query) bool { return testQuery == q.String() })). + Return([]int64{testHeight}, nil) + rpcConfig := config.TestRPCConfig() + l := log.TestingLogger() + d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + + startedWG := &sync.WaitGroup{} + startedWG.Add(1) + go func() { + startedWG.Done() + defer wg.Done() + require.NoError(t, d.Run(ctx)) + }() + // FIXME: used to induce context switch. + // Determine more deterministic method for prompting a context switch + startedWG.Wait() + requireConnect(t, rpcConfig.ListenAddress, 20) + cli, err := httpclient.New(rpcConfig.ListenAddress) + require.NoError(t, err) + + testPage := 1 + testPerPage := 100 + testOrderBy := "desc" + res, err := cli.BlockSearch(context.Background(), testQuery, &testPage, &testPerPage, testOrderBy) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, testBlockHash, []byte(res.Blocks[0].BlockID.Hash)) + + cancel() + wg.Wait() + + blockStoreMock.AssertExpectations(t) + stateStoreMock.AssertExpectations(t) +} + +func requireConnect(t testing.TB, addr string, retries int) { + parts := strings.SplitN(addr, "://", 2) + if len(parts) != 2 { + t.Fatalf("malformed address to dial: %s", addr) + } + var err error + for i := 0; i < retries; i++ { + var conn net.Conn + conn, err = net.Dial(parts[0], parts[1]) + if err == nil { + conn.Close() + return + } + // FIXME attempt to yield and let the other goroutine continue execution. + time.Sleep(time.Microsecond * 100) + } + t.Fatalf("unable to connect to server %s after %d tries: %s", addr, retries, err) +} diff --git a/inspect/rpc/rpc.go b/inspect/rpc/rpc.go new file mode 100644 index 000000000..76dcda4eb --- /dev/null +++ b/inspect/rpc/rpc.go @@ -0,0 +1,143 @@ +package rpc + +import ( + "context" + "net/http" + "time" + + "github.com/rs/cors" + + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/internal/consensus" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/rpc/core" + "github.com/tendermint/tendermint/rpc/jsonrpc/server" + "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/state/indexer" + "github.com/tendermint/tendermint/types" +) + +// Server defines parameters for running an Inspector rpc server. +type Server struct { + Addr string // TCP address to listen on, ":http" if empty + Handler http.Handler + Logger log.Logger + Config *config.RPCConfig +} + +// Routes returns the set of routes used by the Inspector server. +// +//nolint: lll +func Routes(cfg config.RPCConfig, s state.Store, bs state.BlockStore, es []indexer.EventSink, logger log.Logger) core.RoutesMap { + env := &core.Environment{ + Config: cfg, + EventSinks: es, + StateStore: s, + BlockStore: bs, + ConsensusReactor: waitSyncCheckerImpl{}, + Logger: logger, + } + return core.RoutesMap{ + "blockchain": server.NewRPCFunc(env.BlockchainInfo, "minHeight,maxHeight", true), + "consensus_params": server.NewRPCFunc(env.ConsensusParams, "height", true), + "block": server.NewRPCFunc(env.Block, "height", true), + "block_by_hash": server.NewRPCFunc(env.BlockByHash, "hash", true), + "block_results": server.NewRPCFunc(env.BlockResults, "height", true), + "commit": server.NewRPCFunc(env.Commit, "height", true), + "validators": server.NewRPCFunc(env.Validators, "height,page,per_page", true), + "tx": server.NewRPCFunc(env.Tx, "hash,prove", true), + "tx_search": server.NewRPCFunc(env.TxSearch, "query,prove,page,per_page,order_by", false), + "block_search": server.NewRPCFunc(env.BlockSearch, "query,page,per_page,order_by", false), + } +} + +// Handler returns the http.Handler configured for use with an Inspector server. Handler +// registers the routes on the http.Handler and also registers the websocket handler +// and the CORS handler if specified by the configuration options. +func Handler(rpcConfig *config.RPCConfig, routes core.RoutesMap, logger log.Logger) http.Handler { + mux := http.NewServeMux() + wmLogger := logger.With("protocol", "websocket") + + var eventBus types.EventBusSubscriber + + websocketDisconnectFn := func(remoteAddr string) { + err := eventBus.UnsubscribeAll(context.Background(), remoteAddr) + if err != nil && err != pubsub.ErrSubscriptionNotFound { + wmLogger.Error("Failed to unsubscribe addr from events", "addr", remoteAddr, "err", err) + } + } + wm := server.NewWebsocketManager(routes, + server.OnDisconnect(websocketDisconnectFn), + server.ReadLimit(rpcConfig.MaxBodyBytes)) + wm.SetLogger(wmLogger) + mux.HandleFunc("/websocket", wm.WebsocketHandler) + + server.RegisterRPCFuncs(mux, routes, logger) + var rootHandler http.Handler = mux + if rpcConfig.IsCorsEnabled() { + rootHandler = addCORSHandler(rpcConfig, mux) + } + return rootHandler +} + +func addCORSHandler(rpcConfig *config.RPCConfig, h http.Handler) http.Handler { + corsMiddleware := cors.New(cors.Options{ + AllowedOrigins: rpcConfig.CORSAllowedOrigins, + AllowedMethods: rpcConfig.CORSAllowedMethods, + AllowedHeaders: rpcConfig.CORSAllowedHeaders, + }) + h = corsMiddleware.Handler(h) + return h +} + +type waitSyncCheckerImpl struct{} + +func (waitSyncCheckerImpl) WaitSync() bool { + return false +} + +func (waitSyncCheckerImpl) GetPeerState(peerID types.NodeID) (*consensus.PeerState, bool) { + return nil, false +} + +// ListenAndServe listens on the address specified in srv.Addr and handles any +// incoming requests over HTTP using the Inspector rpc handler specified on the server. +func (srv *Server) ListenAndServe(ctx context.Context) error { + listener, err := server.Listen(srv.Addr, srv.Config.MaxOpenConnections) + if err != nil { + return err + } + go func() { + <-ctx.Done() + listener.Close() + }() + return server.Serve(listener, srv.Handler, srv.Logger, serverRPCConfig(srv.Config)) +} + +// ListenAndServeTLS listens on the address specified in srv.Addr. ListenAndServeTLS handles +// incoming requests over HTTPS using the Inspector rpc handler specified on the server. +func (srv *Server) ListenAndServeTLS(ctx context.Context, certFile, keyFile string) error { + listener, err := server.Listen(srv.Addr, srv.Config.MaxOpenConnections) + if err != nil { + return err + } + go func() { + <-ctx.Done() + listener.Close() + }() + return server.ServeTLS(listener, srv.Handler, certFile, keyFile, srv.Logger, serverRPCConfig(srv.Config)) +} + +func serverRPCConfig(r *config.RPCConfig) *server.Config { + cfg := server.DefaultConfig() + cfg.MaxBodyBytes = r.MaxBodyBytes + cfg.MaxHeaderBytes = r.MaxHeaderBytes + // If necessary adjust global WriteTimeout to ensure it's greater than + // TimeoutBroadcastTxCommit. + // See https://github.com/tendermint/tendermint/issues/3435 + if cfg.WriteTimeout <= r.TimeoutBroadcastTxCommit { + cfg.WriteTimeout = r.TimeoutBroadcastTxCommit + 1*time.Second + } + return cfg +} diff --git a/light/proxy/proxy.go b/light/proxy/proxy.go index 8f1e7bf87..6f2622588 100644 --- a/light/proxy/proxy.go +++ b/light/proxy/proxy.go @@ -113,7 +113,7 @@ func (p *Proxy) listen() (net.Listener, *http.ServeMux, error) { } // 4) Start listening for new connections. - listener, err := rpcserver.Listen(p.Addr, p.Config) + listener, err := rpcserver.Listen(p.Addr, p.Config.MaxOpenConnections) if err != nil { return nil, mux, err } diff --git a/node/node.go b/node/node.go index a31362b49..9cb5315bc 100644 --- a/node/node.go +++ b/node/node.go @@ -441,22 +441,23 @@ func makeNode(config *cfg.Config, ProxyAppQuery: proxyApp.Query(), ProxyAppMempool: proxyApp.Mempool(), - StateStore: stateStore, - BlockStore: blockStore, - EvidencePool: evPool, - ConsensusState: csState, + StateStore: stateStore, + BlockStore: blockStore, + EvidencePool: evPool, + ConsensusState: csState, + + ConsensusReactor: csReactor, BlockSyncReactor: bcReactor.(cs.BlockSyncReactor), P2PPeers: sw, PeerManager: peerManager, - GenDoc: genDoc, - EventSinks: eventSinks, - ConsensusReactor: csReactor, - EventBus: eventBus, - Mempool: mp, - Logger: logger.With("module", "rpc"), - Config: *config.RPC, + GenDoc: genDoc, + EventSinks: eventSinks, + EventBus: eventBus, + Mempool: mp, + Logger: logger.With("module", "rpc"), + Config: *config.RPC, }, } @@ -825,7 +826,7 @@ func (n *nodeImpl) startRPC() ([]net.Listener, error) { rpcserver.RegisterRPCFuncs(mux, routes, rpcLogger) listener, err := rpcserver.Listen( listenAddr, - config, + config.MaxOpenConnections, ) if err != nil { return nil, err @@ -883,7 +884,7 @@ func (n *nodeImpl) startRPC() ([]net.Listener, error) { if config.WriteTimeout <= n.config.RPC.TimeoutBroadcastTxCommit { config.WriteTimeout = n.config.RPC.TimeoutBroadcastTxCommit + 1*time.Second } - listener, err := rpcserver.Listen(grpcListenAddr, config) + listener, err := rpcserver.Listen(grpcListenAddr, config.MaxOpenConnections) if err != nil { return nil, err } diff --git a/node/setup.go b/node/setup.go index e9bfb029a..6d2a7523b 100644 --- a/node/setup.go +++ b/node/setup.go @@ -8,7 +8,6 @@ import ( "math" "net" _ "net/http/pprof" // nolint: gosec // securely exposed on separate, optional port - "strings" "time" dbm "github.com/tendermint/tm-db" @@ -33,9 +32,7 @@ import ( "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/state/indexer" - kv "github.com/tendermint/tendermint/state/indexer/sink/kv" - null "github.com/tendermint/tendermint/state/indexer/sink/null" - psql "github.com/tendermint/tendermint/state/indexer/sink/psql" + "github.com/tendermint/tendermint/state/indexer/sink" "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/version" @@ -78,56 +75,9 @@ func createAndStartIndexerService( logger log.Logger, chainID string, ) (*indexer.Service, []indexer.EventSink, error) { - - eventSinks := []indexer.EventSink{} - - // check for duplicated sinks - sinks := map[string]bool{} - for _, s := range config.TxIndex.Indexer { - sl := strings.ToLower(s) - if sinks[sl] { - return nil, nil, errors.New("found duplicated sinks, please check the tx-index section in the config.toml") - } - - sinks[sl] = true - } - -loop: - for k := range sinks { - switch k { - case string(indexer.NULL): - // When we see null in the config, the eventsinks will be reset with the - // nullEventSink. - eventSinks = []indexer.EventSink{null.NewEventSink()} - break loop - - case string(indexer.KV): - store, err := dbProvider(&cfg.DBContext{ID: "tx_index", Config: config}) - if err != nil { - return nil, nil, err - } - - eventSinks = append(eventSinks, kv.NewEventSink(store)) - - case string(indexer.PSQL): - conn := config.TxIndex.PsqlConn - if conn == "" { - return nil, nil, errors.New("the psql connection settings cannot be empty") - } - - es, _, err := psql.NewEventSink(conn, chainID) - if err != nil { - return nil, nil, err - } - eventSinks = append(eventSinks, es) - - default: - return nil, nil, errors.New("unsupported event sink type") - } - } - - if len(eventSinks) == 0 { - eventSinks = []indexer.EventSink{null.NewEventSink()} + eventSinks, err := sink.EventSinksFromConfig(config, dbProvider, chainID) + if err != nil { + return nil, nil, err } indexerService := indexer.NewIndexerService(eventSinks, eventBus) diff --git a/rpc/core/env.go b/rpc/core/env.go index c35b8229c..7069bc4d4 100644 --- a/rpc/core/env.go +++ b/rpc/core/env.go @@ -58,6 +58,11 @@ type peers interface { Peers() p2p.IPeerSet } +type consensusReactor interface { + WaitSync() bool + GetPeerState(peerID types.NodeID) (*consensus.PeerState, bool) +} + type peerManager interface { Peers() []types.NodeID Addresses(types.NodeID) []p2p.NodeAddress @@ -72,11 +77,12 @@ type Environment struct { ProxyAppMempool proxy.AppConnMempool // interfaces defined in types and above - StateStore sm.Store - BlockStore sm.BlockStore - EvidencePool sm.EvidencePool - ConsensusState consensusState - P2PPeers peers + StateStore sm.Store + BlockStore sm.BlockStore + EvidencePool sm.EvidencePool + ConsensusState consensusState + ConsensusReactor consensusReactor + P2PPeers peers // Legacy p2p stack P2PTransport transport @@ -88,7 +94,6 @@ type Environment struct { PubKey crypto.PubKey GenDoc *types.GenesisDoc // cache the genesis structure EventSinks []indexer.EventSink - ConsensusReactor *consensus.Reactor EventBus *types.EventBus // thread safe Mempool mempl.Mempool BlockSyncReactor consensus.BlockSyncReactor diff --git a/rpc/jsonrpc/jsonrpc_test.go b/rpc/jsonrpc/jsonrpc_test.go index 6e0c03f00..b5e422280 100644 --- a/rpc/jsonrpc/jsonrpc_test.go +++ b/rpc/jsonrpc/jsonrpc_test.go @@ -110,7 +110,7 @@ func setup() { wm.SetLogger(tcpLogger) mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) config := server.DefaultConfig() - listener1, err := server.Listen(tcpAddr, config) + listener1, err := server.Listen(tcpAddr, config.MaxOpenConnections) if err != nil { panic(err) } @@ -126,7 +126,7 @@ func setup() { wm = server.NewWebsocketManager(Routes) wm.SetLogger(unixLogger) mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) - listener2, err := server.Listen(unixAddr, config) + listener2, err := server.Listen(unixAddr, config.MaxOpenConnections) if err != nil { panic(err) } diff --git a/rpc/jsonrpc/server/http_server.go b/rpc/jsonrpc/server/http_server.go index c21c71c49..549671241 100644 --- a/rpc/jsonrpc/server/http_server.go +++ b/rpc/jsonrpc/server/http_server.go @@ -261,7 +261,7 @@ func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Listen starts a new net.Listener on the given address. // It returns an error if the address is invalid or the call to Listen() fails. -func Listen(addr string, config *Config) (listener net.Listener, err error) { +func Listen(addr string, maxOpenConnections int) (listener net.Listener, err error) { parts := strings.SplitN(addr, "://", 2) if len(parts) != 2 { return nil, fmt.Errorf( @@ -274,8 +274,8 @@ func Listen(addr string, config *Config) (listener net.Listener, err error) { if err != nil { return nil, fmt.Errorf("failed to listen on %v: %v", addr, err) } - if config.MaxOpenConnections > 0 { - listener = netutil.LimitListener(listener, config.MaxOpenConnections) + if maxOpenConnections > 0 { + listener = netutil.LimitListener(listener, maxOpenConnections) } return listener, nil diff --git a/rpc/jsonrpc/server/http_server_test.go b/rpc/jsonrpc/server/http_server_test.go index e7c517cde..823719e41 100644 --- a/rpc/jsonrpc/server/http_server_test.go +++ b/rpc/jsonrpc/server/http_server_test.go @@ -39,8 +39,7 @@ func TestMaxOpenConnections(t *testing.T) { fmt.Fprint(w, "some body") }) config := DefaultConfig() - config.MaxOpenConnections = max - l, err := Listen("tcp://127.0.0.1:0", config) + l, err := Listen("tcp://127.0.0.1:0", max) require.NoError(t, err) defer l.Close() go Serve(l, mux, log.TestingLogger(), config) //nolint:errcheck // ignore for tests diff --git a/rpc/jsonrpc/test/main.go b/rpc/jsonrpc/test/main.go index 1c949571f..d348e1639 100644 --- a/rpc/jsonrpc/test/main.go +++ b/rpc/jsonrpc/test/main.go @@ -33,7 +33,7 @@ func main() { rpcserver.RegisterRPCFuncs(mux, routes, logger) config := rpcserver.DefaultConfig() - listener, err := rpcserver.Listen("tcp://127.0.0.1:8008", config) + listener, err := rpcserver.Listen("tcp://127.0.0.1:8008", config.MaxOpenConnections) if err != nil { tmos.Exit(err.Error()) } diff --git a/state/indexer/indexer_service.go b/state/indexer/indexer_service.go index a429b66a0..39a1847f8 100644 --- a/state/indexer/indexer_service.go +++ b/state/indexer/indexer_service.go @@ -51,43 +51,47 @@ func (is *Service) OnStart() error { go func() { for { - msg := <-blockHeadersSub.Out() + select { + case <-blockHeadersSub.Canceled(): + return + case msg := <-blockHeadersSub.Out(): - eventDataHeader := msg.Data().(types.EventDataNewBlockHeader) - height := eventDataHeader.Header.Height - batch := NewBatch(eventDataHeader.NumTxs) + eventDataHeader := msg.Data().(types.EventDataNewBlockHeader) + height := eventDataHeader.Header.Height + batch := NewBatch(eventDataHeader.NumTxs) - for i := int64(0); i < eventDataHeader.NumTxs; i++ { - msg2 := <-txsSub.Out() - txResult := msg2.Data().(types.EventDataTx).TxResult + for i := int64(0); i < eventDataHeader.NumTxs; i++ { + msg2 := <-txsSub.Out() + txResult := msg2.Data().(types.EventDataTx).TxResult - if err = batch.Add(&txResult); err != nil { - is.Logger.Error( - "failed to add tx to batch", - "height", height, - "index", txResult.Index, - "err", err, - ) - } - } - - if !IndexingEnabled(is.eventSinks) { - continue - } - - for _, sink := range is.eventSinks { - if err := sink.IndexBlockEvents(eventDataHeader); err != nil { - is.Logger.Error("failed to index block", "height", height, "err", err) - } else { - is.Logger.Debug("indexed block", "height", height, "sink", sink.Type()) + if err = batch.Add(&txResult); err != nil { + is.Logger.Error( + "failed to add tx to batch", + "height", height, + "index", txResult.Index, + "err", err, + ) + } } - if len(batch.Ops) > 0 { - err := sink.IndexTxEvents(batch.Ops) - if err != nil { - is.Logger.Error("failed to index block txs", "height", height, "err", err) + if !IndexingEnabled(is.eventSinks) { + continue + } + + for _, sink := range is.eventSinks { + if err := sink.IndexBlockEvents(eventDataHeader); err != nil { + is.Logger.Error("failed to index block", "height", height, "err", err) } else { - is.Logger.Debug("indexed txs", "height", height, "sink", sink.Type()) + is.Logger.Debug("indexed block", "height", height, "sink", sink.Type()) + } + + if len(batch.Ops) > 0 { + err := sink.IndexTxEvents(batch.Ops) + if err != nil { + is.Logger.Error("failed to index block txs", "height", height, "err", err) + } else { + is.Logger.Debug("indexed txs", "height", height, "sink", sink.Type()) + } } } } diff --git a/state/indexer/sink/sink.go b/state/indexer/sink/sink.go new file mode 100644 index 000000000..1e50721c6 --- /dev/null +++ b/state/indexer/sink/sink.go @@ -0,0 +1,65 @@ +package sink + +import ( + "errors" + "strings" + + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/state/indexer" + "github.com/tendermint/tendermint/state/indexer/sink/kv" + "github.com/tendermint/tendermint/state/indexer/sink/null" + "github.com/tendermint/tendermint/state/indexer/sink/psql" +) + +// EventSinksFromConfig constructs a slice of indexer.EventSink using the provided +// configuration. +// +//nolint:lll +func EventSinksFromConfig(cfg *config.Config, dbProvider config.DBProvider, chainID string) ([]indexer.EventSink, error) { + if len(cfg.TxIndex.Indexer) == 0 { + return []indexer.EventSink{null.NewEventSink()}, nil + } + + // check for duplicated sinks + sinks := map[string]struct{}{} + for _, s := range cfg.TxIndex.Indexer { + sl := strings.ToLower(s) + if _, ok := sinks[sl]; ok { + return nil, errors.New("found duplicated sinks, please check the tx-index section in the config.toml") + } + sinks[sl] = struct{}{} + } + eventSinks := []indexer.EventSink{} + for k := range sinks { + switch indexer.EventSinkType(k) { + case indexer.NULL: + // When we see null in the config, the eventsinks will be reset with the + // nullEventSink. + return []indexer.EventSink{null.NewEventSink()}, nil + + case indexer.KV: + store, err := dbProvider(&config.DBContext{ID: "tx_index", Config: cfg}) + if err != nil { + return nil, err + } + + eventSinks = append(eventSinks, kv.NewEventSink(store)) + + case indexer.PSQL: + conn := cfg.TxIndex.PsqlConn + if conn == "" { + return nil, errors.New("the psql connection settings cannot be empty") + } + + es, _, err := psql.NewEventSink(conn, chainID) + if err != nil { + return nil, err + } + eventSinks = append(eventSinks, es) + default: + return nil, errors.New("unsupported event sink type") + } + } + return eventSinks, nil + +} From 41a361ed8d9755b62972140fc3ddb7a9826e3529 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Tue, 24 Aug 2021 14:18:27 -0700 Subject: [PATCH 12/12] psql: add documentation and simplify constructor API (#6856) Add documentation comments to the psql event sink package, and simplify the constructor function so that it does not return the SQL database handle. The handle is needed for testing, so expose that via a separate method on the concrete type. Update the tests and existing usage for the change. This change does not affect the behaviour of the sink, so there are no functional changes, only syntactic updates. --- cmd/tendermint/commands/reindex_event.go | 4 +-- state/indexer/indexer_service_test.go | 13 ++++----- state/indexer/sink/psql/psql.go | 34 +++++++++++++++++------- state/indexer/sink/psql/psql_test.go | 13 +++------ state/indexer/sink/sink.go | 2 +- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/cmd/tendermint/commands/reindex_event.go b/cmd/tendermint/commands/reindex_event.go index ddc585c1f..1dbce2f74 100644 --- a/cmd/tendermint/commands/reindex_event.go +++ b/cmd/tendermint/commands/reindex_event.go @@ -31,7 +31,7 @@ var ReIndexEventCmd = &cobra.Command{ Long: ` reindex-event is an offline tooling to re-index block and tx events to the eventsinks, you can run this command when the event store backend dropped/disconnected or you want to replace the backend. - The default start-height is 0, meaning the tooling will start reindex from the base block height(inclusive); and the + The default start-height is 0, meaning the tooling will start reindex from the base block height(inclusive); and the default end-height is 0, meaning the tooling will reindex until the latest block height(inclusive). User can omits either or both arguments. `, @@ -106,7 +106,7 @@ func loadEventSinks(cfg *tmcfg.Config) ([]indexer.EventSink, error) { if conn == "" { return nil, errors.New("the psql connection settings cannot be empty") } - es, _, err := psql.NewEventSink(conn, chainID) + es, err := psql.NewEventSink(conn, chainID) if err != nil { return nil, err } diff --git a/state/indexer/indexer_service_test.go b/state/indexer/indexer_service_test.go index 68a00afb5..457ed065a 100644 --- a/state/indexer/indexer_service_test.go +++ b/state/indexer/indexer_service_test.go @@ -164,19 +164,16 @@ func setupDB(t *testing.T) (*dockertest.Pool, error) { conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName) - if err = pool.Retry(func() error { - var err error - - pSink, psqldb, err = psql.NewEventSink(conn, "test-chainID") - + assert.NoError(t, pool.Retry(func() error { + sink, err := psql.NewEventSink(conn, "test-chainID") if err != nil { return err } + pSink = sink + psqldb = sink.DB() return psqldb.Ping() - }); err != nil { - assert.Error(t, err) - } + })) resetDB(t) diff --git a/state/indexer/sink/psql/psql.go b/state/indexer/sink/psql/psql.go index efb539e0b..8bd378f4a 100644 --- a/state/indexer/sink/psql/psql.go +++ b/state/indexer/sink/psql/psql.go @@ -1,3 +1,4 @@ +// Package psql implements an event sink backed by a PostgreSQL database. package psql import ( @@ -24,28 +25,38 @@ const ( DriverName = "postgres" ) -// EventSink is an indexer backend providing the tx/block index services. +// EventSink is an indexer backend providing the tx/block index services. This +// implementation stores records in a PostgreSQL database using the schema +// defined in state/indexer/sink/psql/schema.sql. type EventSink struct { store *sql.DB chainID string } -func NewEventSink(connStr string, chainID string) (indexer.EventSink, *sql.DB, error) { +// NewEventSink constructs an event sink associated with the PostgreSQL +// database specified by connStr. Events written to the sink are attributed to +// the specified chainID. +func NewEventSink(connStr, chainID string) (*EventSink, error) { db, err := sql.Open(DriverName, connStr) if err != nil { - return nil, nil, err + return nil, err } return &EventSink{ store: db, chainID: chainID, - }, db, nil + }, nil } -func (es *EventSink) Type() indexer.EventSinkType { - return indexer.PSQL -} +// DB returns the underlying Postgres connection used by the sink. +// This is exported to support testing. +func (es *EventSink) DB() *sql.DB { return es.store } +// Type returns the structure type for this sink, which is Postgres. +func (es *EventSink) Type() indexer.EventSinkType { return indexer.PSQL } + +// IndexBlockEvents indexes the specified block header, part of the +// indexer.EventSink interface. func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error { sqlStmt := sq. Insert(TableEventBlock). @@ -156,18 +167,22 @@ func (es *EventSink) IndexTxEvents(txr []*abci.TxResult) error { return err } +// SearchBlockEvents is not implemented by this sink, and reports an error for all queries. func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) { return nil, errors.New("block search is not supported via the postgres event sink") } +// SearchTxEvents is not implemented by this sink, and reports an error for all queries. func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) { return nil, errors.New("tx search is not supported via the postgres event sink") } +// GetTxByHash is not implemented by this sink, and reports an error for all queries. func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) { return nil, errors.New("getTxByHash is not supported via the postgres event sink") } +// HasBlock is not implemented by this sink, and reports an error for all queries. func (es *EventSink) HasBlock(h int64) (bool, error) { return false, errors.New("hasBlock is not supported via the postgres event sink") } @@ -206,6 +221,5 @@ func indexBlockEvents( return sqlStmt, nil } -func (es *EventSink) Stop() error { - return es.store.Close() -} +// Stop closes the underlying PostgreSQL database. +func (es *EventSink) Stop() error { return es.store.Close() } diff --git a/state/indexer/sink/psql/psql_test.go b/state/indexer/sink/psql/psql_test.go index 0df773a53..35ad7eea3 100644 --- a/state/indexer/sink/psql/psql_test.go +++ b/state/indexer/sink/psql/psql_test.go @@ -341,19 +341,14 @@ func setupDB(t *testing.T) (*dockertest.Pool, error) { conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName) - if err = pool.Retry(func() error { - var err error - - _, db, err = NewEventSink(conn, chainID) - + require.NoError(t, pool.Retry(func() error { + sink, err := NewEventSink(conn, chainID) if err != nil { return err } - + db = sink.DB() // set global for test use return db.Ping() - }); err != nil { - require.NoError(t, err) - } + })) resetDB(t) diff --git a/state/indexer/sink/sink.go b/state/indexer/sink/sink.go index 1e50721c6..f9dfa54df 100644 --- a/state/indexer/sink/sink.go +++ b/state/indexer/sink/sink.go @@ -51,7 +51,7 @@ func EventSinksFromConfig(cfg *config.Config, dbProvider config.DBProvider, chai return nil, errors.New("the psql connection settings cannot be empty") } - es, _, err := psql.NewEventSink(conn, chainID) + es, err := psql.NewEventSink(conn, chainID) if err != nil { return nil, err }