Merge branch 'master' into wb/tm-signer-harness

This commit is contained in:
William Banfield
2021-12-01 16:46:49 -05:00
committed by GitHub
110 changed files with 2308 additions and 4541 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
go-version: "1.17"
- name: test & coverage report creation
run: |
cat pkgs.txt.part.${{ matrix.part }} | xargs go test -mod=readonly -timeout 8m -race -coverprofile=${{ matrix.part }}profile.out
cat pkgs.txt.part.${{ matrix.part }} | xargs go test -mod=readonly -timeout 15m -race -coverprofile=${{ matrix.part }}profile.out
if: env.GIT_DIFF
- uses: actions/upload-artifact@v2
with:
+6 -6
View File
@@ -28,7 +28,7 @@ jobs:
- name: install
run: make install install_abci
if: "env.GIT_DIFF != ''"
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@@ -36,7 +36,7 @@ jobs:
${{ runner.os }}-go-
if: env.GIT_DIFF
# Cache binaries for use by other jobs
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/bin
key: ${{ runner.os }}-${{ github.sha }}-tm-binary
@@ -57,14 +57,14 @@ jobs:
**/**.go
go.mod
go.sum
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
if: env.GIT_DIFF
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/bin
key: ${{ runner.os }}-${{ github.sha }}-tm-binary
@@ -88,14 +88,14 @@ jobs:
**/**.go
go.mod
go.sum
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
if: env.GIT_DIFF
- uses: actions/cache@v2.1.6
- uses: actions/cache@v2.1.7
with:
path: ~/go/bin
key: ${{ runner.os }}-${{ github.sha }}-tm-binary
+1 -4
View File
@@ -24,7 +24,7 @@ linters:
- govet
- ineffassign
# - interfacer
- lll
# - lll
# - maligned
- misspell
- nakedret
@@ -46,9 +46,6 @@ issues:
- path: _test\.go
linters:
- gosec
- linters:
- lll
source: "https://"
max-same-issues: 50
linters-settings:
+2
View File
@@ -44,6 +44,8 @@ Special thanks to external contributors on this release:
### IMPROVEMENTS
- [pubsub] \#7319 Performance improvements for the event query API (@creachadair)
### BUG FIXES
- fix: assignment copies lock value in `BitArray.UnmarshalJSON()` (@lklimek)
+4 -1
View File
@@ -38,10 +38,13 @@ func NewLocalClient(mtx *tmsync.Mutex, app types.Application) Client {
return cli
}
func (*localClient) OnStart(context.Context) error { return nil }
func (*localClient) OnStop() {}
func (app *localClient) SetResponseCallback(cb Callback) {
app.mtx.Lock()
defer app.mtx.Unlock()
app.Callback = cb
app.mtx.Unlock()
}
// TODO: change types.Application to include Error()?
-16
View File
@@ -680,22 +680,6 @@ func (_m *Client) QuerySync(_a0 context.Context, _a1 types.RequestQuery) (*types
return r0, r1
}
// Quit provides a mock function with given fields:
func (_m *Client) Quit() <-chan struct{} {
ret := _m.Called()
var r0 <-chan struct{}
if rf, ok := ret.Get(0).(func() <-chan struct{}); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(<-chan struct{})
}
}
return r0
}
// SetResponseCallback provides a mock function with given fields: _a0
func (_m *Client) SetResponseCallback(_a0 abciclient.Callback) {
_m.Called(_a0)
-2
View File
@@ -126,8 +126,6 @@ func (cli *socketClient) sendRequestsRoutine(ctx context.Context, conn io.Writer
select {
case <-ctx.Done():
return
case <-cli.Quit():
return
case reqres := <-cli.reqQueue:
if ctx.Err() != nil {
return
@@ -15,6 +15,8 @@ import (
"github.com/tendermint/tendermint/internal/state/mocks"
prototmstate "github.com/tendermint/tendermint/proto/tendermint/state"
"github.com/tendermint/tendermint/types"
_ "github.com/lib/pq" // for the psql sink
)
const (
-1
View File
@@ -105,7 +105,6 @@ func TestStateSyncConfigValidateBasic(t *testing.T) {
}
func TestConsensusConfig_ValidateBasic(t *testing.T) {
// nolint: lll
testcases := map[string]struct {
modify func(*ConsensusConfig)
expectErr bool
+3 -2
View File
@@ -65,7 +65,9 @@ Note the context/background should be written in the present tense.
- [ADR-059: Evidence-Composition-and-Lifecycle](./adr-059-evidence-composition-and-lifecycle.md)
- [ADR-062: P2P-Architecture](./adr-062-p2p-architecture.md)
- [ADR-063: Privval-gRPC](./adr-063-privval-grpc.md)
- [ADR-066-E2E-Testing](./adr-066-e2e-testing.md)
- [ADR-066: E2E-Testing](./adr-066-e2e-testing.md)
- [ADR-072: Restore Requests for Comments](./adr-072-request-for-comments.md)
### Accepted
- [ADR-006: Trust-Metric](./adr-006-trust-metric.md)
@@ -99,4 +101,3 @@ Note the context/background should be written in the present tense.
- [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)
@@ -6,7 +6,7 @@
## Status
Proposed
Implemented
## Context
+235
View File
@@ -0,0 +1,235 @@
# ADR 073: Adopt LibP2P
## Changelog
- 2021-11-02: Initial Draft (@tychoish)
## Status
Proposed.
## Context
As part of the 0.35 development cycle, the Tendermint team completed
the first phase of the work described in ADRs 61 and 62, which included a
large scale refactoring of the reactors and the p2p message
routing. This replaced the switch and many of the other legacy
components without breaking protocol or network-level
interoperability and left the legacy connection/socket handling code.
Following the release, the team has reexamined the state of the code
and the design, as well as Tendermint's requirements. The notes
from that process are available in the [P2P Roadmap
RFC][rfc].
This ADR supersedes the decisions made in ADRs 60 and 61, but
builds on the completed portions of this work. Previously, the
boundaries of peer management, message handling, and the higher level
business logic (e.g., "the reactors") were intermingled, and core
elements of the p2p system were responsible for the orchestration of
higher-level business logic. Refactoring the legacy components
made it more obvious that this entanglement of responsibilities
had outsized influence on the entire implementation, making
it difficult to iterate within the current abstractions.
It would not be viable to maintain interoperability with legacy
systems while also achieving many of our broader objectives.
LibP2P is a thoroughly-specified implementation of a peer-to-peer
networking stack, designed specifically for systems such as
ours. Adopting LibP2P as the basis of Tendermint will allow the
Tendermint team to focus more of their time on other differentiating
aspects of the system, and make it possible for the ecosystem as a
whole to take advantage of tooling and efforts of the LibP2P
platform.
## Alternative Approaches
As discussed in the [P2P Roadmap RFC][rfc], the primary alternative would be to
continue development of Tendermint's home-grown peer-to-peer
layer. While that would give the Tendermint team maximal control
over the peer system, the current design is unexceptional on its
own merits, and the prospective maintenance burden for this system
exceeds our tolerances for the medium term.
Tendermint can and should differentiate itself not on the basis of
its networking implementation or peer management tools, but providing
a consistent operator experience, a battle-tested consensus algorithm,
and an ergonomic user experience.
## Decision
Tendermint will adopt libp2p during the 0.37 development cycle,
replacing the bespoke Tendermint P2P stack. This will remove the
`Endpoint`, `Transport`, `Connection`, and `PeerManager` abstractions
and leave the reactors, `p2p.Router` and `p2p.Channel`
abstractions.
LibP2P may obviate the need for a dedicated peer exchange (PEX)
reactor, which would also in turn obviate the need for a dedicated
seed mode. If this is the case, then all of this functionality would
be removed.
If it turns out (based on the advice of Protocol Labs) that it makes
sense to maintain separate pubsub or gossipsub topics
per-message-type, then the `Router` abstraction could also
be entirely subsumed.
## Detailed Design
### Implementation Changes
The seams in the P2P implementation between the higher level
constructs (reactors), the routing layer (`Router`) and the lower
level connection and peer management code make this operation
relatively straightforward to implement. A key
goal in this design is to minimize the impact on the reactors
(potentially entirely,) and completely remove the lower level
components (e.g., `Transport`, `Connection` and `PeerManager`) using the
separation afforded by the `Router` layer. The current state of the
code makes these changes relatively surgical, and limited to a small
number of methods:
- `p2p.Router.OpenChannel` will still return a `Channel` structure
which will continue to serve as a pipe between the reactors and the
`Router`. The implementation will no longer need the queue
implementation, and will instead start goroutines that
are responsible for routing the messages from the channel to libp2p
fundamentals, replacing the current `p2p.Router.routeChannel`.
- The current `p2p.Router.dialPeers` and `p2p.Router.acceptPeers`,
are responsible for establishing outbound and inbound connections,
respectively. These methods will be removed, along with
`p2p.Router.openConnection`, and the libp2p connection manager will
be responsible for maintaining network connectivity.
- The `p2p.Channel` interface will change to replace Go
channels with a more functional interface for sending messages.
New methods on this object will take contexts to support safe
cancellation, and return errors, and will block rather than
running asynchronously. The `Out` channel through which
reactors send messages to Peers, will be replaced by a `Send`
method, and the Error channel will be replaced by an `Error`
method.
- Reactors will be passed an interface that will allow them to
access Peer information from libp2p. This will supplant the
`p2p.PeerUpdates` subscription.
- Add some kind of heartbeat message at the application level
(e.g. with a reactor,) potentially connected to libp2p's DHT to be
used by reactors for service discovery, message targeting, or other
features.
- Replace the existing/legacy handshake protocol with [Noise](http://www.noiseprotocol.org/noise.html).
This project will initially use the TCP-based transport protocols within
libp2p. QUIC is also available as an option that we may implement later.
We will not support mixed networks in the initial release, but will
revisit that possibility later if there is a demonstrated need.
### Upgrade and Compatibility
Because the routers and all current P2P libraries are `internal`
packages and not part of the public API, the only changes to the public
API surface area of Tendermint will be different configuration
file options, replacing the current P2P options with options relevant
to libp2p.
However, it will not be possible to run a network with both networking
stacks active at once, so the upgrade to the version of Tendermint
will need to be coordinated between all nodes of the network. This is
consistent with the expectations around upgrades for Tendermint moving
forward, and will help manage both the complexity of the project and
the implementation timeline.
## Open Questions
- What is the role of Protocol Labs in the implementation of libp2p in
tendermint, both during the initial implementation and on an ongoing
basis thereafter?
- Should all P2P traffic for a given node be pushed to a single topic,
so that a topic maps to a specific ChainID, or should
each reactor (or type of message) have its own topic? How many
topics can a libp2p network support? Is there testing that validates
the capabilities?
- Tendermint presently provides a very coarse QoS-like functionality
using priorities based on message-type.
This intuitively/theoretically ensures that evidence and consensus
messages don't get starved by blocksync/statesync messages. It's
unclear if we can or should attempt to replicate this with libp2p.
- What kind of QoS functionality does libp2p provide and what kind of
metrics does libp2p provide about it's QoS functionality?
- Is it possible to store additional (and potentially arbitrary)
information into the DHT as part of the heartbeats between nodes,
such as the latest height, and then access that in the
reactors. How frequently can the DHT be updated?
- Does it make sense to have reactors continue to consume inbound
messages from a Channel (`In`) or is there another interface or
pattern that we should consider?
- We should avoid exposing Go channels when possible, and likely
some kind of alternate iterator likely makes sense for processing
messages within the reactors.
- What are the security and protocol implications of tracking
information from peer heartbeats and exposing that to reactors?
- How much (or how little) configuration can Tendermint provide for
libp2p, particularly on the first release?
- In general, we should not support byo-functionality for libp2p
components within Tendermint, and reduce the configuration surface
area, as much as possible.
- What are the best ways to provide request/response semantics for
reactors on top of libp2p? Will it be possible to add
request/response semantics in a future release or is there
anticipatory work that needs to be done as part of the initial
release?
## Consequences
### Positive
- Reduce the maintenance burden for the Tendermint Core team by
removing a large swath of legacy code that has proven to be
difficult to modify safely.
- Remove the responsibility for maintaining and developing the entire
peer management system (p2p) and stack.
- Provide users with a more stable peer and networking system,
Tendermint can improve operator experience and network stability.
### Negative
- By deferring to library implementations for peer management and
networking, Tendermint loses some flexibility for innovating at the
peer and networking level. However, Tendermint should be innovating
primarily at the consensus layer, and libp2p does not preclude
optimization or development in the peer layer.
- Libp2p is a large dependency and Tendermint would become dependent
upon Protocol Labs' release cycle and prioritization for bug
fixes. If this proves onerous, it's possible to maintain a vendor
fork of relevant components as needed.
### Neutral
- N/A
## References
- [ADR 61: P2P Refactor Scope][adr61]
- [ADR 62: P2P Architecture][adr62]
- [P2P Roadmap RFC][rfc]
[adr61]: ./adr-061-p2p-refactor-scope.md
[adr62]: ./adr-062-p2p-architecture.md
[rfc]: ../rfc/rfc-000-p2p.rst
+3 -3
View File
@@ -10069,9 +10069,9 @@
}
},
"watchpack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz",
"integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz",
"integrity": "sha512-MnN0Q1OsvB/GGHETrFeZPQaOelWh/7O+EiFlj8sM9GPjtQkis7k01aAxrg/18kTfoIVcLL+haEVFlXDaSRwKRw==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"vuepress-theme-cosmos": "^1.0.182"
},
"devDependencies": {
"watchpack": "^2.2.0"
"watchpack": "^2.3.0"
},
"scripts": {
"preserve": "./pre.sh",
+2 -2
View File
@@ -367,7 +367,7 @@ func main() {
flag.Parse()
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
logger := log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false)
server := abciserver.NewSocketServer(socketAddr, app)
server.SetLogger(logger)
@@ -438,7 +438,7 @@ This should create a `go.mod` file. The current tutorial only works with
the master branch of Tendermint, so let's make sure we're using the latest version:
```sh
go get github.com/tendermint/tendermint@master
go get github.com/tendermint/tendermint@97a3e44e0724f2017079ce24d36433f03124c09e
```
This will populate the `go.mod` with a release number followed by a hash for Tendermint.
+27 -10
View File
@@ -84,6 +84,7 @@ type BlockPool struct {
requestsCh chan<- BlockRequest
errorsCh chan<- peerError
exitedCh chan struct{}
startHeight int64
lastHundredBlockTimeStamp time.Time
@@ -102,11 +103,11 @@ func NewBlockPool(
bp := &BlockPool{
peers: make(map[types.NodeID]*bpPeer),
requesters: make(map[int64]*bpRequester),
height: start,
startHeight: start,
numPending: 0,
requesters: make(map[int64]*bpRequester),
height: start,
startHeight: start,
numPending: 0,
exitedCh: make(chan struct{}),
requestsCh: requestsCh,
errorsCh: errorsCh,
lastSyncRate: 0,
@@ -121,9 +122,17 @@ func (pool *BlockPool) OnStart(ctx context.Context) error {
pool.lastAdvance = time.Now()
pool.lastHundredBlockTimeStamp = pool.lastAdvance
go pool.makeRequestersRoutine(ctx)
go func() {
defer close(pool.exitedCh)
pool.Wait()
}()
return nil
}
func (*BlockPool) OnStop() {}
// spawns requesters as needed
func (pool *BlockPool) makeRequestersRoutine(ctx context.Context) {
for {
@@ -572,10 +581,12 @@ func newBPRequester(pool *BlockPool, height int64) *bpRequester {
}
func (bpr *bpRequester) OnStart(ctx context.Context) error {
go bpr.requestRoutine()
go bpr.requestRoutine(ctx)
return nil
}
func (*bpRequester) OnStop() {}
// Returns true if the peer matches and block doesn't already exist.
func (bpr *bpRequester) setBlock(block *types.Block, peerID types.NodeID) bool {
bpr.mtx.Lock()
@@ -630,7 +641,13 @@ func (bpr *bpRequester) redo(peerID types.NodeID) {
// Responsible for making more requests as necessary
// Returns only when a block is found (e.g. AddBlock() is called)
func (bpr *bpRequester) requestRoutine() {
func (bpr *bpRequester) requestRoutine(ctx context.Context) {
bprPoolDone := make(chan struct{})
go func() {
defer close(bprPoolDone)
bpr.pool.Wait()
}()
OUTER_LOOP:
for {
// Pick a peer to send request to.
@@ -656,13 +673,13 @@ OUTER_LOOP:
WAIT_LOOP:
for {
select {
case <-bpr.pool.Quit():
case <-ctx.Done():
return
case <-bpr.pool.exitedCh:
if err := bpr.Stop(); err != nil {
bpr.Logger.Error("Error stopped requester", "err", err)
}
return
case <-bpr.Quit():
return
case peerID := <-bpr.redoCh:
if peerID == bpr.peerID {
bpr.reset()
+6 -6
View File
@@ -158,7 +158,7 @@ func (r *Reactor) OnStart(ctx context.Context) error {
return err
}
r.poolWG.Add(1)
go r.requestRoutine()
go r.requestRoutine(ctx)
r.poolWG.Add(1)
go r.poolRoutine(false)
@@ -375,7 +375,7 @@ func (r *Reactor) SwitchToBlockSync(ctx context.Context, state sm.State) error {
r.syncStartTime = time.Now()
r.poolWG.Add(1)
go r.requestRoutine()
go r.requestRoutine(ctx)
r.poolWG.Add(1)
go r.poolRoutine(true)
@@ -383,7 +383,7 @@ func (r *Reactor) SwitchToBlockSync(ctx context.Context, state sm.State) error {
return nil
}
func (r *Reactor) requestRoutine() {
func (r *Reactor) requestRoutine(ctx context.Context) {
statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second)
defer statusUpdateTicker.Stop()
@@ -394,7 +394,7 @@ func (r *Reactor) requestRoutine() {
case <-r.closeCh:
return
case <-r.pool.Quit():
case <-ctx.Done():
return
case request := <-r.requestsCh:
@@ -580,7 +580,7 @@ FOR_LOOP:
// TODO: Same thing for app - but we would need a way to get the hash
// without persisting the state.
state, err = r.blockExec.ApplyBlock(state, firstID, first)
state, err = r.blockExec.ApplyBlock(ctx, state, firstID, first)
if err != nil {
// TODO: This is bad, are we zombie?
panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err))
@@ -607,7 +607,7 @@ FOR_LOOP:
case <-r.closeCh:
break FOR_LOOP
case <-r.pool.Quit():
case <-r.pool.exitedCh:
break FOR_LOOP
}
}
+1 -1
View File
@@ -158,7 +158,7 @@ func (rts *reactorTestSuite) addNode(
thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes)
blockID := types.BlockID{Hash: thisBlock.Hash(), PartSetHeader: thisParts.Header()}
state, err = blockExec.ApplyBlock(state, blockID, thisBlock)
state, err = blockExec.ApplyBlock(ctx, state, blockID, thisBlock)
require.NoError(t, err)
blockStore.SaveBlock(thisBlock, thisParts, lastCommit)
+1 -1
View File
@@ -90,7 +90,7 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
// Make State
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore)
cs := NewState(logger, thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool)
cs := NewState(ctx, logger, thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool)
// set private validator
pv := privVals[i]
cs.SetPrivValidator(pv)
+12 -2
View File
@@ -224,7 +224,7 @@ func (vss ValidatorStubsByPower) Swap(i, j int) {
// Functions for transitioning the consensus state
func startTestRound(ctx context.Context, cs *State, height int64, round int32) {
cs.enterNewRound(height, round)
cs.enterNewRound(ctx, height, round)
cs.startRoutines(ctx, 0)
}
@@ -467,7 +467,15 @@ func newStateWithConfigAndBlockStore(
}
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyAppConnCon, mempool, evpool, blockStore)
cs := NewState(logger.With("module", "consensus"), thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool)
cs := NewState(ctx,
logger.With("module", "consensus"),
thisConfig.Consensus,
state,
blockExec,
blockStore,
mempool,
evpool,
)
cs.SetPrivValidator(pv)
eventBus := eventbus.NewDefault(logger.With("module", "events"))
@@ -907,6 +915,8 @@ func (m *mockTicker) Stop() error {
return nil
}
func (m *mockTicker) IsRunning() bool { return false }
func (m *mockTicker) ScheduleTimeout(ti timeoutInfo) {
m.mtx.Lock()
defer m.mtx.Unlock()
-1
View File
@@ -313,7 +313,6 @@ func TestWALMsgProto(t *testing.T) {
}
}
// nolint:lll //ignore line length for tests
func TestConsMsgsVectors(t *testing.T) {
date := time.Date(2018, 8, 30, 12, 0, 0, 0, time.UTC)
psh := types.PartSetHeader{
+6 -4
View File
@@ -2,6 +2,7 @@ package consensus
import (
"context"
"errors"
"fmt"
"runtime/debug"
"time"
@@ -205,11 +206,12 @@ func (r *Reactor) OnStart(ctx context.Context) error {
// blocking until they all exit, as well as unsubscribing from events and stopping
// state.
func (r *Reactor) OnStop() {
r.unsubscribeFromBroadcastEvents()
if err := r.state.Stop(); err != nil {
r.Logger.Error("failed to stop consensus state", "err", err)
if !errors.Is(err, service.ErrAlreadyStopped) {
r.Logger.Error("failed to stop consensus state", "err", err)
}
}
if !r.WaitSync() {
@@ -275,7 +277,7 @@ func (r *Reactor) SwitchToConsensus(ctx context.Context, state sm.State, skipWAL
// NOTE: The line below causes broadcastNewRoundStepRoutine() to broadcast a
// NewRoundStepMessage.
r.state.updateToState(state)
r.state.updateToState(ctx, state)
r.mtx.Lock()
r.waitSync = false
@@ -299,7 +301,7 @@ conR:
}
d := types.EventDataBlockSyncStatus{Complete: true, Height: state.LastBlockHeight}
if err := r.eventBus.PublishEventBlockSyncStatus(d); err != nil {
if err := r.eventBus.PublishEventBlockSyncStatus(ctx, d); err != nil {
r.Logger.Error("failed to emit the blocksync complete event", "err", err)
}
}
+1 -1
View File
@@ -421,7 +421,7 @@ func TestReactorWithEvidence(t *testing.T) {
evpool2 := sm.EmptyEvidencePool{}
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyAppConnCon, mempool, evpool, blockStore)
cs := NewState(logger.With("validator", i, "module", "consensus"),
cs := NewState(ctx, logger.With("validator", i, "module", "consensus"),
thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool2)
cs.SetPrivValidator(pv)
+21 -15
View File
@@ -38,7 +38,7 @@ var crc32c = crc32.MakeTable(crc32.Castagnoli)
// Unmarshal and apply a single message to the consensus state as if it were
// received in receiveRoutine. Lines that start with "#" are ignored.
// NOTE: receiveRoutine should not be running.
func (cs *State) readReplayMessage(msg *TimedWALMessage, newStepSub eventbus.Subscription) error {
func (cs *State) readReplayMessage(ctx context.Context, msg *TimedWALMessage, newStepSub eventbus.Subscription) error {
// Skip meta messages which exist for demarcating boundaries.
if _, ok := msg.Msg.(EndHeightMessage); ok {
return nil
@@ -50,7 +50,7 @@ func (cs *State) readReplayMessage(msg *TimedWALMessage, newStepSub eventbus.Sub
cs.Logger.Info("Replay: New Step", "height", m.Height, "round", m.Round, "step", m.Step)
// these are playback checks
if newStepSub != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
stepMsg, err := newStepSub.Next(ctx)
if errors.Is(err, context.DeadlineExceeded) {
@@ -81,10 +81,10 @@ func (cs *State) readReplayMessage(msg *TimedWALMessage, newStepSub eventbus.Sub
"blockID", v.BlockID, "peer", peerID)
}
cs.handleMsg(m)
cs.handleMsg(ctx, m)
case timeoutInfo:
cs.Logger.Info("Replay: Timeout", "height", m.Height, "round", m.Round, "step", m.Step, "dur", m.Duration)
cs.handleTimeout(m, cs.RoundState)
cs.handleTimeout(ctx, m, cs.RoundState)
default:
return fmt.Errorf("replay: Unknown TimedWALMessage type: %v", reflect.TypeOf(msg.Msg))
}
@@ -93,7 +93,7 @@ func (cs *State) readReplayMessage(msg *TimedWALMessage, newStepSub eventbus.Sub
// Replay only those messages since the last block. `timeoutRoutine` should
// run concurrently to read off tickChan.
func (cs *State) catchupReplay(csHeight int64) error {
func (cs *State) catchupReplay(ctx context.Context, csHeight int64) error {
// Set replayMode to true so we don't log signing errors.
cs.replayMode = true
@@ -160,7 +160,7 @@ LOOP:
// NOTE: since the priv key is set when the msgs are received
// it will attempt to eg double sign but we can just ignore it
// since the votes will be replayed and we'll get to the next step
if err := cs.readReplayMessage(msg, nil); err != nil {
if err := cs.readReplayMessage(ctx, msg, nil); err != nil {
return err
}
}
@@ -390,7 +390,7 @@ func (h *Handshaker) ReplayBlocks(
// Either the app is asking for replay, or we're all synced up.
if appBlockHeight < storeBlockHeight {
// the app is behind, so replay blocks, but no need to go through WAL (state is already synced to store)
return h.replayBlocks(state, proxyApp, appBlockHeight, storeBlockHeight, false)
return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, false)
} else if appBlockHeight == storeBlockHeight {
// We're good!
@@ -405,7 +405,7 @@ func (h *Handshaker) ReplayBlocks(
case appBlockHeight < stateBlockHeight:
// the app is further behind than it should be, so replay blocks
// but leave the last block to go through the WAL
return h.replayBlocks(state, proxyApp, appBlockHeight, storeBlockHeight, true)
return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, true)
case appBlockHeight == stateBlockHeight:
// We haven't run Commit (both the state and app are one block behind),
@@ -413,7 +413,7 @@ func (h *Handshaker) ReplayBlocks(
// NOTE: We could instead use the cs.WAL on cs.Start,
// but we'd have to allow the WAL to replay a block that wrote it's #ENDHEIGHT
h.logger.Info("Replay last block using real app")
state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus())
state, err = h.replayBlock(ctx, state, storeBlockHeight, proxyApp.Consensus())
return state.AppHash, err
case appBlockHeight == storeBlockHeight:
@@ -424,7 +424,7 @@ func (h *Handshaker) ReplayBlocks(
}
mockApp := newMockProxyApp(ctx, h.logger, appHash, abciResponses)
h.logger.Info("Replay last block using mock app")
state, err = h.replayBlock(state, storeBlockHeight, mockApp)
state, err = h.replayBlock(ctx, state, storeBlockHeight, mockApp)
return state.AppHash, err
}
@@ -435,6 +435,7 @@ func (h *Handshaker) ReplayBlocks(
}
func (h *Handshaker) replayBlocks(
ctx context.Context,
state sm.State,
proxyApp proxy.AppConns,
appBlockHeight,
@@ -474,13 +475,13 @@ func (h *Handshaker) replayBlocks(
blockExec := sm.NewBlockExecutor(
h.stateStore, h.logger, proxyApp.Consensus(), emptyMempool{}, sm.EmptyEvidencePool{}, h.store)
blockExec.SetEventBus(h.eventBus)
appHash, err = sm.ExecCommitBlock(
appHash, err = sm.ExecCommitBlock(ctx,
blockExec, proxyApp.Consensus(), block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
if err != nil {
return nil, err
}
} else {
appHash, err = sm.ExecCommitBlock(
appHash, err = sm.ExecCommitBlock(ctx,
nil, proxyApp.Consensus(), block, h.logger, h.stateStore, h.genDoc.InitialHeight, state)
if err != nil {
return nil, err
@@ -492,7 +493,7 @@ func (h *Handshaker) replayBlocks(
if mutateState {
// sync the final block
state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus())
state, err = h.replayBlock(ctx, state, storeBlockHeight, proxyApp.Consensus())
if err != nil {
return nil, err
}
@@ -504,7 +505,12 @@ func (h *Handshaker) replayBlocks(
}
// ApplyBlock on the proxyApp with the last block.
func (h *Handshaker) replayBlock(state sm.State, height int64, proxyApp proxy.AppConnConsensus) (sm.State, error) {
func (h *Handshaker) replayBlock(
ctx context.Context,
state sm.State,
height int64,
proxyApp proxy.AppConnConsensus,
) (sm.State, error) {
block := h.store.LoadBlock(height)
meta := h.store.LoadBlockMeta(height)
@@ -514,7 +520,7 @@ func (h *Handshaker) replayBlock(state sm.State, height int64, proxyApp proxy.Ap
blockExec.SetEventBus(h.eventBus)
var err error
state, err = blockExec.ApplyBlock(state, meta.BlockID, block)
state, err = blockExec.ApplyBlock(ctx, state, meta.BlockID, block)
if err != nil {
return sm.State{}, err
}
+7 -7
View File
@@ -104,7 +104,7 @@ func (cs *State) ReplayFile(ctx context.Context, file string, console bool) erro
return err
}
if err := pb.cs.readReplayMessage(msg, newStepSub); err != nil {
if err := pb.cs.readReplayMessage(ctx, msg, newStepSub); err != nil {
return err
}
@@ -141,13 +141,13 @@ func newPlayback(fileName string, fp *os.File, cs *State, genState sm.State) *pl
}
// go back count steps by resetting the state and running (pb.count - count) steps
func (pb *playback) replayReset(count int, newStepSub eventbus.Subscription) error {
func (pb *playback) replayReset(ctx context.Context, count int, newStepSub eventbus.Subscription) error {
if err := pb.cs.Stop(); err != nil {
return err
}
pb.cs.Wait()
newCS := NewState(pb.cs.Logger, pb.cs.config, pb.genesisState.Copy(), pb.cs.blockExec,
newCS := NewState(ctx, pb.cs.Logger, pb.cs.config, pb.genesisState.Copy(), pb.cs.blockExec,
pb.cs.blockStore, pb.cs.txNotifier, pb.cs.evpool)
newCS.SetEventBus(pb.cs.eventBus)
newCS.startForReplay()
@@ -173,7 +173,7 @@ func (pb *playback) replayReset(count int, newStepSub eventbus.Subscription) err
} else if err != nil {
return err
}
if err := pb.cs.readReplayMessage(msg, newStepSub); err != nil {
if err := pb.cs.readReplayMessage(ctx, msg, newStepSub); err != nil {
return err
}
pb.count++
@@ -254,7 +254,7 @@ func (pb *playback) replayConsoleLoop() (int, error) {
}()
if len(tokens) == 1 {
if err := pb.replayReset(1, newStepSub); err != nil {
if err := pb.replayReset(ctx, 1, newStepSub); err != nil {
pb.cs.Logger.Error("Replay reset error", "err", err)
}
} else {
@@ -263,7 +263,7 @@ func (pb *playback) replayConsoleLoop() (int, error) {
fmt.Println("back takes an integer argument")
} else if i > pb.count {
fmt.Printf("argument to back must not be larger than the current count (%d)\n", pb.count)
} else if err := pb.replayReset(i, newStepSub); err != nil {
} else if err := pb.replayReset(ctx, i, newStepSub); err != nil {
pb.cs.Logger.Error("Replay reset error", "err", err)
}
}
@@ -359,7 +359,7 @@ func newConsensusStateForReplay(
mempool, evpool := emptyMempool{}, sm.EmptyEvidencePool{}
blockExec := sm.NewBlockExecutor(stateStore, logger, proxyApp.Consensus(), mempool, evpool, blockStore)
consensusState := NewState(logger, csConfig, state.Copy(), blockExec,
consensusState := NewState(ctx, logger, csConfig, state.Copy(), blockExec,
blockStore, mempool, evpool)
consensusState.SetEventBus(eventBus)
+12 -9
View File
@@ -855,18 +855,21 @@ func testHandshakeReplay(
}
}
func applyBlock(stateStore sm.Store,
func applyBlock(
ctx context.Context,
stateStore sm.Store,
mempool mempool.Mempool,
evpool sm.EvidencePool,
st sm.State,
blk *types.Block,
proxyApp proxy.AppConns,
blockStore *mockBlockStore) sm.State {
blockStore *mockBlockStore,
) sm.State {
testPartSize := types.BlockPartSizeBytes
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool, blockStore)
blkID := types.BlockID{Hash: blk.Hash(), PartSetHeader: blk.MakePartSet(testPartSize).Header()}
newState, err := blockExec.ApplyBlock(st, blkID, blk)
newState, err := blockExec.ApplyBlock(ctx, st, blkID, blk)
if err != nil {
panic(err)
}
@@ -904,18 +907,18 @@ func buildAppStateFromChain(
case 0:
for i := 0; i < nBlocks; i++ {
block := chain[i]
state = applyBlock(stateStore, mempool, evpool, state, block, proxyApp, blockStore)
state = applyBlock(ctx, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
}
case 1, 2, 3:
for i := 0; i < nBlocks-1; i++ {
block := chain[i]
state = applyBlock(stateStore, mempool, evpool, state, block, proxyApp, blockStore)
state = applyBlock(ctx, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
}
if mode == 2 || mode == 3 {
// update the kvstore height and apphash
// as if we ran commit but not
state = applyBlock(stateStore, mempool, evpool, state, chain[nBlocks-1], proxyApp, blockStore)
state = applyBlock(ctx, stateStore, mempool, evpool, state, chain[nBlocks-1], proxyApp, blockStore)
}
default:
panic(fmt.Sprintf("unknown mode %v", mode))
@@ -961,19 +964,19 @@ func buildTMStateFromChain(
case 0:
// sync right up
for _, block := range chain {
state = applyBlock(stateStore, mempool, evpool, state, block, proxyApp, blockStore)
state = applyBlock(ctx, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
}
case 1, 2, 3:
// sync up to the penultimate as if we stored the block.
// whether we commit or not depends on the appHash
for _, block := range chain[:len(chain)-1] {
state = applyBlock(stateStore, mempool, evpool, state, block, proxyApp, blockStore)
state = applyBlock(ctx, stateStore, mempool, evpool, state, block, proxyApp, blockStore)
}
// apply the final block to a state copy so we can
// get the right next appHash but keep the state back
applyBlock(stateStore, mempool, evpool, state, chain[len(chain)-1], proxyApp, blockStore)
applyBlock(ctx, stateStore, mempool, evpool, state, chain[len(chain)-1], proxyApp, blockStore)
default:
panic(fmt.Sprintf("unknown mode %v", mode))
}
+123 -94
View File
@@ -153,6 +153,7 @@ type StateOption func(*State)
// NewState returns a new State.
func NewState(
ctx context.Context,
logger log.Logger,
cfg *config.ConsensusConfig,
state sm.State,
@@ -190,7 +191,7 @@ func NewState(
cs.reconstructLastCommit(state)
}
cs.updateToState(state)
cs.updateToState(ctx, state)
// NOTE: we do not call scheduleRound0 yet, we do that upon Start()
@@ -345,7 +346,7 @@ func (cs *State) OnStart(ctx context.Context) error {
LOOP:
for {
err := cs.catchupReplay(cs.Height)
err := cs.catchupReplay(ctx, cs.Height)
switch {
case err == nil:
break LOOP
@@ -362,6 +363,7 @@ func (cs *State) OnStart(ctx context.Context) error {
// 1) prep work
if err := cs.wal.Stop(); err != nil {
return err
}
@@ -409,7 +411,7 @@ func (cs *State) OnStart(ctx context.Context) error {
}
// now start the receiveRoutine
go cs.receiveRoutine(0)
go cs.receiveRoutine(ctx, 0)
// schedule the first round!
// use GetRoundState so we don't race the receiveRoutine for access
@@ -420,6 +422,8 @@ func (cs *State) OnStart(ctx context.Context) error {
// timeoutRoutine: receive requests for timeouts on tickChan and fire timeouts on tockChan
// receiveRoutine: serializes processing of proposoals, block parts, votes; coordinates state transitions
//
// this is only used in tests.
func (cs *State) startRoutines(ctx context.Context, maxSteps int) {
err := cs.timeoutTicker.Start(ctx)
if err != nil {
@@ -427,7 +431,7 @@ func (cs *State) startRoutines(ctx context.Context, maxSteps int) {
return
}
go cs.receiveRoutine(maxSteps)
go cs.receiveRoutine(ctx, maxSteps)
}
// loadWalFile loads WAL data from file. It overwrites cs.wal.
@@ -444,7 +448,6 @@ func (cs *State) loadWalFile(ctx context.Context) error {
// OnStop implements service.Service.
func (cs *State) OnStop() {
// If the node is committing a new block, wait until it is finished!
if cs.GetRoundState().Step == cstypes.RoundStepCommit {
select {
@@ -456,15 +459,19 @@ func (cs *State) OnStop() {
close(cs.onStopCh)
if err := cs.evsw.Stop(); err != nil {
if !errors.Is(err, service.ErrAlreadyStopped) {
cs.Logger.Error("failed trying to stop eventSwitch", "error", err)
if cs.evsw.IsRunning() {
if err := cs.evsw.Stop(); err != nil {
if !errors.Is(err, service.ErrAlreadyStopped) {
cs.Logger.Error("failed trying to stop eventSwitch", "error", err)
}
}
}
if err := cs.timeoutTicker.Stop(); err != nil {
if !errors.Is(err, service.ErrAlreadyStopped) {
cs.Logger.Error("failed trying to stop timeoutTicket", "error", err)
if cs.timeoutTicker.IsRunning() {
if err := cs.timeoutTicker.Stop(); err != nil {
if !errors.Is(err, service.ErrAlreadyStopped) {
cs.Logger.Error("failed trying to stop timeoutTicket", "error", err)
}
}
}
// WAL is stopped in receiveRoutine.
@@ -625,7 +632,7 @@ func (cs *State) reconstructLastCommit(state sm.State) {
// Updates State and increments height to match that of state.
// The round becomes 0 and cs.Step becomes cstypes.RoundStepNewHeight.
func (cs *State) updateToState(state sm.State) {
func (cs *State) updateToState(ctx context.Context, state sm.State) {
if cs.CommitRound > -1 && 0 < cs.Height && cs.Height != state.LastBlockHeight {
panic(fmt.Sprintf(
"updateToState() expected state height of %v but found %v",
@@ -660,7 +667,7 @@ func (cs *State) updateToState(state sm.State) {
"new_height", state.LastBlockHeight+1,
"old_height", cs.state.LastBlockHeight+1,
)
cs.newStep()
cs.newStep(ctx)
return
}
}
@@ -729,10 +736,10 @@ func (cs *State) updateToState(state sm.State) {
cs.state = state
// Finally, broadcast RoundState
cs.newStep()
cs.newStep(ctx)
}
func (cs *State) newStep() {
func (cs *State) newStep(ctx context.Context) {
rs := cs.RoundStateEvent()
if err := cs.wal.Write(rs); err != nil {
cs.Logger.Error("failed writing to WAL", "err", err)
@@ -742,7 +749,7 @@ func (cs *State) newStep() {
// newStep is called by updateToState in NewState before the eventBus is set!
if cs.eventBus != nil {
if err := cs.eventBus.PublishEventNewRoundStep(rs); err != nil {
if err := cs.eventBus.PublishEventNewRoundStep(ctx, rs); err != nil {
cs.Logger.Error("failed publishing new round step", "err", err)
}
@@ -758,7 +765,7 @@ func (cs *State) newStep() {
// It keeps the RoundState and is the only thing that updates it.
// Updates (state transitions) happen on timeouts, complete proposals, and 2/3 majorities.
// State must be locked before any internal state is updated.
func (cs *State) receiveRoutine(maxSteps int) {
func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) {
onExit := func(cs *State) {
// NOTE: the internalMsgQueue may have signed messages from our
// priv_val that haven't hit the WAL, but its ok because
@@ -804,7 +811,7 @@ func (cs *State) receiveRoutine(maxSteps int) {
select {
case <-cs.txNotifier.TxsAvailable():
cs.handleTxsAvailable()
cs.handleTxsAvailable(ctx)
case mi = <-cs.peerMsgQueue:
if err := cs.wal.Write(mi); err != nil {
@@ -813,7 +820,7 @@ func (cs *State) receiveRoutine(maxSteps int) {
// handles proposals, block parts, votes
// may generate internal events (votes, complete proposals, 2/3 majorities)
cs.handleMsg(mi)
cs.handleMsg(ctx, mi)
case mi = <-cs.internalMsgQueue:
err := cs.wal.WriteSync(mi) // NOTE: fsync
@@ -833,7 +840,7 @@ func (cs *State) receiveRoutine(maxSteps int) {
}
// handles proposals, block parts, votes
cs.handleMsg(mi)
cs.handleMsg(ctx, mi)
case ti := <-cs.timeoutTicker.Chan(): // tockChan:
if err := cs.wal.Write(ti); err != nil {
@@ -842,17 +849,19 @@ func (cs *State) receiveRoutine(maxSteps int) {
// if the timeout is relevant to the rs
// go to the next step
cs.handleTimeout(ti, rs)
cs.handleTimeout(ctx, ti, rs)
case <-cs.Quit():
case <-ctx.Done():
onExit(cs)
return
}
// TODO should we handle context cancels here?
}
}
// state transitions on complete-proposal, 2/3-any, 2/3-one
func (cs *State) handleMsg(mi msgInfo) {
func (cs *State) handleMsg(ctx context.Context, mi msgInfo) {
cs.mtx.Lock()
defer cs.mtx.Unlock()
@@ -871,9 +880,13 @@ func (cs *State) handleMsg(mi msgInfo) {
case *BlockPartMessage:
// if the proposal is complete, we'll enterPrevote or tryFinalizeCommit
added, err = cs.addProposalBlockPart(msg, peerID)
added, err = cs.addProposalBlockPart(ctx, msg, peerID)
if added {
cs.statsMsgQueue <- mi
select {
case cs.statsMsgQueue <- mi:
case <-ctx.Done():
return
}
}
if err != nil && msg.Round != cs.Round {
@@ -889,9 +902,13 @@ func (cs *State) handleMsg(mi msgInfo) {
case *VoteMessage:
// attempt to add the vote and dupeout the validator if its a duplicate signature
// if the vote gives us a 2/3-any or 2/3-one, we transition
added, err = cs.tryAddVote(msg.Vote, peerID)
added, err = cs.tryAddVote(ctx, msg.Vote, peerID)
if added {
cs.statsMsgQueue <- mi
select {
case cs.statsMsgQueue <- mi:
case <-ctx.Done():
return
}
}
// if err == ErrAddingVote {
@@ -926,7 +943,11 @@ func (cs *State) handleMsg(mi msgInfo) {
}
}
func (cs *State) handleTimeout(ti timeoutInfo, rs cstypes.RoundState) {
func (cs *State) handleTimeout(
ctx context.Context,
ti timeoutInfo,
rs cstypes.RoundState,
) {
cs.Logger.Debug("received tock", "timeout", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step)
// timeouts must be for current height, round, step
@@ -943,32 +964,32 @@ func (cs *State) handleTimeout(ti timeoutInfo, rs cstypes.RoundState) {
case cstypes.RoundStepNewHeight:
// NewRound event fired from enterNewRound.
// XXX: should we fire timeout here (for timeout commit)?
cs.enterNewRound(ti.Height, 0)
cs.enterNewRound(ctx, ti.Height, 0)
case cstypes.RoundStepNewRound:
cs.enterPropose(ti.Height, 0)
cs.enterPropose(ctx, ti.Height, 0)
case cstypes.RoundStepPropose:
if err := cs.eventBus.PublishEventTimeoutPropose(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventTimeoutPropose(ctx, cs.RoundStateEvent()); err != nil {
cs.Logger.Error("failed publishing timeout propose", "err", err)
}
cs.enterPrevote(ti.Height, ti.Round)
cs.enterPrevote(ctx, ti.Height, ti.Round)
case cstypes.RoundStepPrevoteWait:
if err := cs.eventBus.PublishEventTimeoutWait(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventTimeoutWait(ctx, cs.RoundStateEvent()); err != nil {
cs.Logger.Error("failed publishing timeout wait", "err", err)
}
cs.enterPrecommit(ti.Height, ti.Round)
cs.enterPrecommit(ctx, ti.Height, ti.Round)
case cstypes.RoundStepPrecommitWait:
if err := cs.eventBus.PublishEventTimeoutWait(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventTimeoutWait(ctx, cs.RoundStateEvent()); err != nil {
cs.Logger.Error("failed publishing timeout wait", "err", err)
}
cs.enterPrecommit(ti.Height, ti.Round)
cs.enterNewRound(ti.Height, ti.Round+1)
cs.enterPrecommit(ctx, ti.Height, ti.Round)
cs.enterNewRound(ctx, ti.Height, ti.Round+1)
default:
panic(fmt.Sprintf("invalid timeout step: %v", ti.Step))
@@ -976,7 +997,7 @@ func (cs *State) handleTimeout(ti timeoutInfo, rs cstypes.RoundState) {
}
func (cs *State) handleTxsAvailable() {
func (cs *State) handleTxsAvailable(ctx context.Context) {
cs.mtx.Lock()
defer cs.mtx.Unlock()
@@ -997,7 +1018,7 @@ func (cs *State) handleTxsAvailable() {
cs.scheduleTimeout(timeoutCommit, cs.Height, 0, cstypes.RoundStepNewRound)
case cstypes.RoundStepNewRound: // after timeoutCommit
cs.enterPropose(cs.Height, 0)
cs.enterPropose(ctx, cs.Height, 0)
}
}
@@ -1006,12 +1027,12 @@ func (cs *State) handleTxsAvailable() {
// Used internally by handleTimeout and handleMsg to make state transitions
// Enter: `timeoutNewHeight` by startTime (commitTime+timeoutCommit),
// or, if SkipTimeoutCommit==true, after receiving all precommits from (height,round-1)
// or, if SkipTimeoutCommit==true, after receiving all precommits from (height,round-1)
// Enter: `timeoutPrecommits` after any +2/3 precommits from (height,round-1)
// Enter: +2/3 precommits for nil at (height,round-1)
// Enter: +2/3 prevotes any or +2/3 precommits for block or any from (height, round)
// NOTE: cs.StartTime was already set for height.
func (cs *State) enterNewRound(height int64, round int32) {
func (cs *State) enterNewRound(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cs.Step != cstypes.RoundStepNewHeight) {
@@ -1054,7 +1075,7 @@ func (cs *State) enterNewRound(height int64, round int32) {
cs.Votes.SetRound(tmmath.SafeAddInt32(round, 1)) // also track next round (round+1) to allow round-skipping
cs.TriggeredTimeoutPrecommit = false
if err := cs.eventBus.PublishEventNewRound(cs.NewRoundEvent()); err != nil {
if err := cs.eventBus.PublishEventNewRound(ctx, cs.NewRoundEvent()); err != nil {
cs.Logger.Error("failed publishing new round", "err", err)
}
@@ -1070,7 +1091,7 @@ func (cs *State) enterNewRound(height int64, round int32) {
cstypes.RoundStepNewRound)
}
} else {
cs.enterPropose(height, round)
cs.enterPropose(ctx, height, round)
}
}
@@ -1091,9 +1112,9 @@ func (cs *State) needProofBlock(height int64) bool {
// Enter (CreateEmptyBlocks): from enterNewRound(height,round)
// Enter (CreateEmptyBlocks, CreateEmptyBlocksInterval > 0 ):
// after enterNewRound(height,round), after timeout of CreateEmptyBlocksInterval
// after enterNewRound(height,round), after timeout of CreateEmptyBlocksInterval
// Enter (!CreateEmptyBlocks) : after enterNewRound(height,round), once txs are in the mempool
func (cs *State) enterPropose(height int64, round int32) {
func (cs *State) enterPropose(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPropose <= cs.Step) {
@@ -1109,13 +1130,13 @@ func (cs *State) enterPropose(height int64, round int32) {
defer func() {
// Done enterPropose:
cs.updateRoundStep(round, cstypes.RoundStepPropose)
cs.newStep()
cs.newStep(ctx)
// If we have the whole proposal + POL, then goto Prevote now.
// else, we'll enterPrevote when the rest of the proposal is received (in AddProposalBlockPart),
// or else after timeoutPropose
if cs.isProposalComplete() {
cs.enterPrevote(height, cs.Round)
cs.enterPrevote(ctx, height, cs.Round)
}
}()
@@ -1271,7 +1292,7 @@ func (cs *State) createProposalBlock() (block *types.Block, blockParts *types.Pa
// Enter: proposal block and POL is ready.
// Prevote for LockedBlock if we're locked, or ProposalBlock if valid.
// Otherwise vote nil.
func (cs *State) enterPrevote(height int64, round int32) {
func (cs *State) enterPrevote(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrevote <= cs.Step) {
@@ -1285,7 +1306,7 @@ func (cs *State) enterPrevote(height int64, round int32) {
defer func() {
// Done enterPrevote:
cs.updateRoundStep(round, cstypes.RoundStepPrevote)
cs.newStep()
cs.newStep(ctx)
}()
logger.Debug("entering prevote step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
@@ -1331,7 +1352,7 @@ func (cs *State) defaultDoPrevote(height int64, round int32) {
}
// Enter: any +2/3 prevotes at next round.
func (cs *State) enterPrevoteWait(height int64, round int32) {
func (cs *State) enterPrevoteWait(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrevoteWait <= cs.Step) {
@@ -1354,7 +1375,7 @@ func (cs *State) enterPrevoteWait(height int64, round int32) {
defer func() {
// Done enterPrevoteWait:
cs.updateRoundStep(round, cstypes.RoundStepPrevoteWait)
cs.newStep()
cs.newStep(ctx)
}()
// Wait for some more prevotes; enterPrecommit
@@ -1367,7 +1388,7 @@ func (cs *State) enterPrevoteWait(height int64, round int32) {
// Lock & precommit the ProposalBlock if we have enough prevotes for it (a POL in this round)
// else, unlock an existing lock and precommit nil if +2/3 of prevotes were nil,
// else, precommit nil otherwise.
func (cs *State) enterPrecommit(height int64, round int32) {
func (cs *State) enterPrecommit(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cstypes.RoundStepPrecommit <= cs.Step) {
@@ -1383,7 +1404,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
defer func() {
// Done enterPrecommit:
cs.updateRoundStep(round, cstypes.RoundStepPrecommit)
cs.newStep()
cs.newStep(ctx)
}()
// check for a polka
@@ -1402,7 +1423,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
}
// At this point +2/3 prevoted for a particular block or nil.
if err := cs.eventBus.PublishEventPolka(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventPolka(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing polka", "err", err)
}
@@ -1422,7 +1443,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
cs.LockedBlock = nil
cs.LockedBlockParts = nil
if err := cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventUnlock(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event unlock", "err", err)
}
}
@@ -1438,7 +1459,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
logger.Debug("precommit step; +2/3 prevoted locked block; relocking")
cs.LockedRound = round
if err := cs.eventBus.PublishEventRelock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventRelock(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event relock", "err", err)
}
@@ -1459,7 +1480,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
cs.LockedBlock = cs.ProposalBlock
cs.LockedBlockParts = cs.ProposalBlockParts
if err := cs.eventBus.PublishEventLock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventLock(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event lock", "err", err)
}
@@ -1481,7 +1502,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader)
}
if err := cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventUnlock(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event unlock", "err", err)
}
@@ -1489,7 +1510,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
}
// Enter: any +2/3 precommits for next round.
func (cs *State) enterPrecommitWait(height int64, round int32) {
func (cs *State) enterPrecommitWait(ctx context.Context, height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
if cs.Height != height || round < cs.Round || (cs.Round == round && cs.TriggeredTimeoutPrecommit) {
@@ -1513,7 +1534,7 @@ func (cs *State) enterPrecommitWait(height int64, round int32) {
defer func() {
// Done enterPrecommitWait:
cs.TriggeredTimeoutPrecommit = true
cs.newStep()
cs.newStep(ctx)
}()
// wait for some more precommits; enterNewRound
@@ -1521,7 +1542,7 @@ func (cs *State) enterPrecommitWait(height int64, round int32) {
}
// Enter: +2/3 precommits for block
func (cs *State) enterCommit(height int64, commitRound int32) {
func (cs *State) enterCommit(ctx context.Context, height int64, commitRound int32) {
logger := cs.Logger.With("height", height, "commit_round", commitRound)
if cs.Height != height || cstypes.RoundStepCommit <= cs.Step {
@@ -1540,10 +1561,10 @@ func (cs *State) enterCommit(height int64, commitRound int32) {
cs.updateRoundStep(cs.Round, cstypes.RoundStepCommit)
cs.CommitRound = commitRound
cs.CommitTime = tmtime.Now()
cs.newStep()
cs.newStep(ctx)
// Maybe finalize immediately.
cs.tryFinalizeCommit(height)
cs.tryFinalizeCommit(ctx, height)
}()
blockID, ok := cs.Votes.Precommits(commitRound).TwoThirdsMajority()
@@ -1574,7 +1595,7 @@ func (cs *State) enterCommit(height int64, commitRound int32) {
cs.ProposalBlock = nil
cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader)
if err := cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventValidBlock(ctx, cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing valid block", "err", err)
}
@@ -1584,7 +1605,7 @@ func (cs *State) enterCommit(height int64, commitRound int32) {
}
// If we have the block AND +2/3 commits for it, finalize.
func (cs *State) tryFinalizeCommit(height int64) {
func (cs *State) tryFinalizeCommit(ctx context.Context, height int64) {
logger := cs.Logger.With("height", height)
if cs.Height != height {
@@ -1608,11 +1629,11 @@ func (cs *State) tryFinalizeCommit(height int64) {
return
}
cs.finalizeCommit(height)
cs.finalizeCommit(ctx, height)
}
// Increment height and goto cstypes.RoundStepNewHeight
func (cs *State) finalizeCommit(height int64) {
func (cs *State) finalizeCommit(ctx context.Context, height int64) {
logger := cs.Logger.With("height", height)
if cs.Height != height || cs.Step != cstypes.RoundStepCommit {
@@ -1692,7 +1713,7 @@ func (cs *State) finalizeCommit(height int64) {
// Execute and commit the block, update and save the state, and update the mempool.
// NOTE The block.AppHash wont reflect these txs until the next block.
stateCopy, err := cs.blockExec.ApplyBlock(
stateCopy, err := cs.blockExec.ApplyBlock(ctx,
stateCopy,
types.BlockID{
Hash: block.Hash(),
@@ -1711,7 +1732,7 @@ func (cs *State) finalizeCommit(height int64) {
cs.RecordMetrics(height, block)
// NewHeightStep!
cs.updateToState(stateCopy)
cs.updateToState(ctx, stateCopy)
fail.Fail() // XXX
@@ -1864,7 +1885,11 @@ func (cs *State) defaultSetProposal(proposal *types.Proposal) error {
// NOTE: block is not necessarily valid.
// Asynchronously triggers either enterPrevote (before we timeout of propose) or tryFinalizeCommit,
// once we have the full block.
func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID types.NodeID) (added bool, err error) {
func (cs *State) addProposalBlockPart(
ctx context.Context,
msg *BlockPartMessage,
peerID types.NodeID,
) (added bool, err error) {
height, round, part := msg.Height, msg.Round, msg.Part
// Blocks might be reused, so round mismatch is OK
@@ -1918,7 +1943,7 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID types.NodeID
// NOTE: it's possible to receive complete proposal blocks for future rounds without having the proposal
cs.Logger.Info("received complete proposal block", "height", cs.ProposalBlock.Height, "hash", cs.ProposalBlock.Hash())
if err := cs.eventBus.PublishEventCompleteProposal(cs.CompleteProposalEvent()); err != nil {
if err := cs.eventBus.PublishEventCompleteProposal(ctx, cs.CompleteProposalEvent()); err != nil {
cs.Logger.Error("failed publishing event complete proposal", "err", err)
}
@@ -1946,13 +1971,13 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID types.NodeID
if cs.Step <= cstypes.RoundStepPropose && cs.isProposalComplete() {
// Move onto the next step
cs.enterPrevote(height, cs.Round)
cs.enterPrevote(ctx, height, cs.Round)
if hasTwoThirds { // this is optimisation as this will be triggered when prevote is added
cs.enterPrecommit(height, cs.Round)
cs.enterPrecommit(ctx, height, cs.Round)
}
} else if cs.Step == cstypes.RoundStepCommit {
// If we're waiting on the proposal block...
cs.tryFinalizeCommit(height)
cs.tryFinalizeCommit(ctx, height)
}
return added, nil
@@ -1962,8 +1987,8 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID types.NodeID
}
// Attempt to add the vote. if its a duplicate signature, dupeout the validator
func (cs *State) tryAddVote(vote *types.Vote, peerID types.NodeID) (bool, error) {
added, err := cs.addVote(vote, peerID)
func (cs *State) tryAddVote(ctx context.Context, vote *types.Vote, peerID types.NodeID) (bool, error) {
added, err := cs.addVote(ctx, vote, peerID)
if err != nil {
// If the vote height is off, we'll just ignore it,
// But if it's a conflicting sig, add it to the cs.evpool.
@@ -2001,7 +2026,7 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID types.NodeID) (bool, error)
// 1) bad peer OR
// 2) not a bad peer? this can also err sometimes with "Unexpected step" OR
// 3) tmkms use with multiple validators connecting to a single tmkms instance
// (https://github.com/tendermint/tendermint/issues/3839).
// (https://github.com/tendermint/tendermint/issues/3839).
cs.Logger.Info("failed attempting to add vote", "err", err)
return added, ErrAddingVote
}
@@ -2010,7 +2035,11 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID types.NodeID) (bool, error)
return added, nil
}
func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err error) {
func (cs *State) addVote(
ctx context.Context,
vote *types.Vote,
peerID types.NodeID,
) (added bool, err error) {
cs.Logger.Debug(
"adding vote",
"vote_height", vote.Height,
@@ -2034,7 +2063,7 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
}
cs.Logger.Debug("added vote to last precommits", "last_commit", cs.LastCommit.StringShort())
if err := cs.eventBus.PublishEventVote(types.EventDataVote{Vote: vote}); err != nil {
if err := cs.eventBus.PublishEventVote(ctx, types.EventDataVote{Vote: vote}); err != nil {
return added, err
}
@@ -2044,7 +2073,7 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
if cs.config.SkipTimeoutCommit && cs.LastCommit.HasAll() {
// go straight to new round (skip timeout commit)
// cs.scheduleTimeout(time.Duration(0), cs.Height, 0, cstypes.RoundStepNewHeight)
cs.enterNewRound(cs.Height, 0)
cs.enterNewRound(ctx, cs.Height, 0)
}
return
@@ -2064,7 +2093,7 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
return
}
if err := cs.eventBus.PublishEventVote(types.EventDataVote{Vote: vote}); err != nil {
if err := cs.eventBus.PublishEventVote(ctx, types.EventDataVote{Vote: vote}); err != nil {
return added, err
}
cs.evsw.FireEvent(types.EventVoteValue, vote)
@@ -2093,7 +2122,7 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
cs.LockedBlock = nil
cs.LockedBlockParts = nil
if err := cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventUnlock(ctx, cs.RoundStateEvent()); err != nil {
return added, err
}
}
@@ -2122,7 +2151,7 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
}
cs.evsw.FireEvent(types.EventValidBlockValue, &cs.RoundState)
if err := cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()); err != nil {
if err := cs.eventBus.PublishEventValidBlock(ctx, cs.RoundStateEvent()); err != nil {
return added, err
}
}
@@ -2132,20 +2161,20 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
switch {
case cs.Round < vote.Round && prevotes.HasTwoThirdsAny():
// Round-skip if there is any 2/3+ of votes ahead of us
cs.enterNewRound(height, vote.Round)
cs.enterNewRound(ctx, height, vote.Round)
case cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step: // current round
blockID, ok := prevotes.TwoThirdsMajority()
if ok && (cs.isProposalComplete() || len(blockID.Hash) == 0) {
cs.enterPrecommit(height, vote.Round)
cs.enterPrecommit(ctx, height, vote.Round)
} else if prevotes.HasTwoThirdsAny() {
cs.enterPrevoteWait(height, vote.Round)
cs.enterPrevoteWait(ctx, height, vote.Round)
}
case cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round:
// If the proposal is now complete, enter prevote of cs.Round.
if cs.isProposalComplete() {
cs.enterPrevote(height, cs.Round)
cs.enterPrevote(ctx, height, cs.Round)
}
}
@@ -2161,20 +2190,20 @@ func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err
blockID, ok := precommits.TwoThirdsMajority()
if ok {
// Executed as TwoThirdsMajority could be from a higher round
cs.enterNewRound(height, vote.Round)
cs.enterPrecommit(height, vote.Round)
cs.enterNewRound(ctx, height, vote.Round)
cs.enterPrecommit(ctx, height, vote.Round)
if len(blockID.Hash) != 0 {
cs.enterCommit(height, vote.Round)
cs.enterCommit(ctx, height, vote.Round)
if cs.config.SkipTimeoutCommit && precommits.HasAll() {
cs.enterNewRound(cs.Height, 0)
cs.enterNewRound(ctx, cs.Height, 0)
}
} else {
cs.enterPrecommitWait(height, vote.Round)
cs.enterPrecommitWait(ctx, height, vote.Round)
}
} else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() {
cs.enterNewRound(height, vote.Round)
cs.enterPrecommitWait(height, vote.Round)
cs.enterNewRound(ctx, height, vote.Round)
cs.enterPrecommitWait(ctx, height, vote.Round)
}
default:
+11 -11
View File
@@ -191,7 +191,7 @@ func TestStateEnterProposeYesPrivValidator(t *testing.T) {
timeoutCh := subscribe(ctx, t, cs.eventBus, types.EventQueryTimeoutPropose)
proposalCh := subscribe(ctx, t, cs.eventBus, types.EventQueryCompleteProposal)
cs.enterNewRound(height, round)
cs.enterNewRound(ctx, height, round)
cs.startRoutines(ctx, 3)
ensureNewProposal(proposalCh, height, round)
@@ -399,7 +399,7 @@ func TestStateFullRoundNil(t *testing.T) {
voteCh := subscribe(ctx, t, cs.eventBus, types.EventQueryVote)
cs.enterPrevote(height, round)
cs.enterPrevote(ctx, height, round)
cs.startRoutines(ctx, 4)
ensurePrevote(voteCh, height, round) // prevote
@@ -479,7 +479,7 @@ func TestStateLockNoPOL(t *testing.T) {
*/
// start round and wait for prevote
cs1.enterNewRound(height, round)
cs1.enterNewRound(ctx, height, round)
cs1.startRoutines(ctx, 0)
ensureNewRound(newRoundCh, height, round)
@@ -1986,26 +1986,26 @@ func TestStateOutputsBlockPartsStats(t *testing.T) {
}
cs.ProposalBlockParts = types.NewPartSetFromHeader(parts.Header())
cs.handleMsg(msgInfo{msg, peerID})
cs.handleMsg(ctx, msgInfo{msg, peerID})
statsMessage := <-cs.statsMsgQueue
require.Equal(t, msg, statsMessage.Msg, "")
require.Equal(t, peerID, statsMessage.PeerID, "")
// sending the same part from different peer
cs.handleMsg(msgInfo{msg, "peer2"})
cs.handleMsg(ctx, msgInfo{msg, "peer2"})
// sending the part with the same height, but different round
msg.Round = 1
cs.handleMsg(msgInfo{msg, peerID})
cs.handleMsg(ctx, msgInfo{msg, peerID})
// sending the part from the smaller height
msg.Height = 0
cs.handleMsg(msgInfo{msg, peerID})
cs.handleMsg(ctx, msgInfo{msg, peerID})
// sending the part from the bigger height
msg.Height = 3
cs.handleMsg(msgInfo{msg, peerID})
cs.handleMsg(ctx, msgInfo{msg, peerID})
select {
case <-cs.statsMsgQueue:
@@ -2031,20 +2031,20 @@ func TestStateOutputVoteStats(t *testing.T) {
vote := signVote(ctx, vss[1], config, tmproto.PrecommitType, randBytes, types.PartSetHeader{})
voteMessage := &VoteMessage{vote}
cs.handleMsg(msgInfo{voteMessage, peerID})
cs.handleMsg(ctx, msgInfo{voteMessage, peerID})
statsMessage := <-cs.statsMsgQueue
require.Equal(t, voteMessage, statsMessage.Msg, "")
require.Equal(t, peerID, statsMessage.PeerID, "")
// sending the same part from different peer
cs.handleMsg(msgInfo{&VoteMessage{vote}, "peer2"})
cs.handleMsg(ctx, msgInfo{&VoteMessage{vote}, "peer2"})
// sending the vote for the bigger height
incrementHeight(vss[1])
vote = signVote(ctx, vss[1], config, tmproto.PrecommitType, randBytes, types.PartSetHeader{})
cs.handleMsg(msgInfo{&VoteMessage{vote}, peerID})
cs.handleMsg(ctx, msgInfo{&VoteMessage{vote}, peerID})
select {
case <-cs.statsMsgQueue:
+6 -8
View File
@@ -18,6 +18,7 @@ var (
type TimeoutTicker interface {
Start(context.Context) error
Stop() error
IsRunning() bool
Chan() <-chan timeoutInfo // on which to receive a timeout
ScheduleTimeout(ti timeoutInfo) // reset the timer
}
@@ -48,17 +49,14 @@ func NewTimeoutTicker(logger log.Logger) TimeoutTicker {
}
// OnStart implements service.Service. It starts the timeout routine.
func (t *timeoutTicker) OnStart(gctx context.Context) error {
go t.timeoutRoutine()
func (t *timeoutTicker) OnStart(ctx context.Context) error {
go t.timeoutRoutine(ctx)
return nil
}
// OnStop implements service.Service. It stops the timeout routine.
func (t *timeoutTicker) OnStop() {
t.BaseService.OnStop()
t.stopTimer()
}
func (t *timeoutTicker) OnStop() { t.stopTimer() }
// Chan returns a channel on which timeouts are sent.
func (t *timeoutTicker) Chan() <-chan timeoutInfo {
@@ -89,7 +87,7 @@ func (t *timeoutTicker) stopTimer() {
// send on tickChan to start a new timer.
// timers are interupted and replaced by new ticks from later steps
// timeouts of 0 on the tickChan will be immediately relayed to the tockChan
func (t *timeoutTicker) timeoutRoutine() {
func (t *timeoutTicker) timeoutRoutine(ctx context.Context) {
t.Logger.Debug("Starting timeout routine")
var ti timeoutInfo
for {
@@ -125,7 +123,7 @@ func (t *timeoutTicker) timeoutRoutine() {
// We can eliminate it by merging the timeoutRoutine into receiveRoutine
// and managing the timeouts ourselves with a millisecond ticker
go func(toi timeoutInfo) { t.tockChan <- toi }(ti)
case <-t.Quit():
case <-ctx.Done():
return
}
}
+9 -4
View File
@@ -131,18 +131,18 @@ func (wal *BaseWAL) OnStart(ctx context.Context) error {
return err
}
wal.flushTicker = time.NewTicker(wal.flushInterval)
go wal.processFlushTicks()
go wal.processFlushTicks(ctx)
return nil
}
func (wal *BaseWAL) processFlushTicks() {
func (wal *BaseWAL) processFlushTicks(ctx context.Context) {
for {
select {
case <-wal.flushTicker.C:
if err := wal.FlushAndSync(); err != nil {
wal.Logger.Error("Periodic WAL flush failed", "err", err)
}
case <-wal.Quit():
case <-ctx.Done():
return
}
}
@@ -175,7 +175,12 @@ func (wal *BaseWAL) OnStop() {
// Wait for the underlying autofile group to finish shutting down
// so it's safe to cleanup files.
func (wal *BaseWAL) Wait() {
wal.group.Wait()
if wal.IsRunning() {
wal.BaseService.Wait()
}
if wal.group.IsRunning() {
wal.group.Wait()
}
}
// Write is called in newStep and for each receive on the
+1 -1
View File
@@ -80,7 +80,7 @@ func WALGenerateNBlocks(ctx context.Context, t *testing.T, wr io.Writer, numBloc
mempool := emptyMempool{}
evpool := sm.EmptyEvidencePool{}
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool, blockStore)
consensusState := NewState(logger, cfg.Consensus, state.Copy(), blockExec, blockStore, mempool, evpool)
consensusState := NewState(ctx, logger, cfg.Consensus, state.Copy(), blockExec, blockStore, mempool, evpool)
consensusState.SetEventBus(eventBus)
if privValidator != nil && privValidator != (*privval.FilePV)(nil) {
consensusState.SetPrivValidator(privValidator)
+41 -55
View File
@@ -2,7 +2,6 @@ package eventbus
import (
"context"
"errors"
"fmt"
"strings"
@@ -43,13 +42,7 @@ func (b *EventBus) OnStart(ctx context.Context) error {
return b.pubsub.Start(ctx)
}
func (b *EventBus) OnStop() {
if err := b.pubsub.Stop(); err != nil {
if !errors.Is(err, service.ErrAlreadyStopped) {
b.pubsub.Logger.Error("error trying to stop eventBus", "error", err)
}
}
}
func (b *EventBus) OnStop() {}
func (b *EventBus) NumClients() int {
return b.pubsub.NumClients()
@@ -82,10 +75,7 @@ func (b *EventBus) Observe(ctx context.Context, observe func(tmpubsub.Message) e
return b.pubsub.Observe(ctx, observe, queries...)
}
func (b *EventBus) Publish(eventValue string, eventData types.TMEventData) error {
// no explicit deadline for publishing events
ctx := context.Background()
func (b *EventBus) Publish(ctx context.Context, eventValue string, eventData types.TMEventData) error {
tokens := strings.Split(types.EventTypeKey, ".")
event := abci.Event{
Type: tokens[0],
@@ -100,9 +90,7 @@ func (b *EventBus) Publish(eventValue string, eventData types.TMEventData) error
return b.pubsub.PublishWithEvents(ctx, eventData, []abci.Event{event})
}
func (b *EventBus) PublishEventNewBlock(data types.EventDataNewBlock) error {
// no explicit deadline for publishing events
ctx := context.Background()
func (b *EventBus) PublishEventNewBlock(ctx context.Context, data types.EventDataNewBlock) error {
events := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...)
// add Tendermint-reserved new block event
@@ -111,9 +99,9 @@ func (b *EventBus) PublishEventNewBlock(data types.EventDataNewBlock) error {
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewBlockHeader(data types.EventDataNewBlockHeader) error {
func (b *EventBus) PublishEventNewBlockHeader(ctx context.Context, data types.EventDataNewBlockHeader) error {
// no explicit deadline for publishing events
ctx := context.Background()
events := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...)
// add Tendermint-reserved new block header event
@@ -122,32 +110,30 @@ func (b *EventBus) PublishEventNewBlockHeader(data types.EventDataNewBlockHeader
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewEvidence(evidence types.EventDataNewEvidence) error {
return b.Publish(types.EventNewEvidenceValue, evidence)
func (b *EventBus) PublishEventNewEvidence(ctx context.Context, evidence types.EventDataNewEvidence) error {
return b.Publish(ctx, types.EventNewEvidenceValue, evidence)
}
func (b *EventBus) PublishEventVote(data types.EventDataVote) error {
return b.Publish(types.EventVoteValue, data)
func (b *EventBus) PublishEventVote(ctx context.Context, data types.EventDataVote) error {
return b.Publish(ctx, types.EventVoteValue, data)
}
func (b *EventBus) PublishEventValidBlock(data types.EventDataRoundState) error {
return b.Publish(types.EventValidBlockValue, data)
func (b *EventBus) PublishEventValidBlock(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventValidBlockValue, data)
}
func (b *EventBus) PublishEventBlockSyncStatus(data types.EventDataBlockSyncStatus) error {
return b.Publish(types.EventBlockSyncStatusValue, data)
func (b *EventBus) PublishEventBlockSyncStatus(ctx context.Context, data types.EventDataBlockSyncStatus) error {
return b.Publish(ctx, types.EventBlockSyncStatusValue, data)
}
func (b *EventBus) PublishEventStateSyncStatus(data types.EventDataStateSyncStatus) error {
return b.Publish(types.EventStateSyncStatusValue, data)
func (b *EventBus) PublishEventStateSyncStatus(ctx context.Context, data types.EventDataStateSyncStatus) error {
return b.Publish(ctx, types.EventStateSyncStatusValue, data)
}
// PublishEventTx publishes tx event with events from Result. Note it will add
// predefined keys (EventTypeKey, TxHashKey). Existing events with the same keys
// will be overwritten.
func (b *EventBus) PublishEventTx(data types.EventDataTx) error {
// no explicit deadline for publishing events
ctx := context.Background()
func (b *EventBus) PublishEventTx(ctx context.Context, data types.EventDataTx) error {
events := data.Result.Events
// add Tendermint-reserved events
@@ -178,44 +164,44 @@ func (b *EventBus) PublishEventTx(data types.EventDataTx) error {
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewRoundStep(data types.EventDataRoundState) error {
return b.Publish(types.EventNewRoundStepValue, data)
func (b *EventBus) PublishEventNewRoundStep(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventNewRoundStepValue, data)
}
func (b *EventBus) PublishEventTimeoutPropose(data types.EventDataRoundState) error {
return b.Publish(types.EventTimeoutProposeValue, data)
func (b *EventBus) PublishEventTimeoutPropose(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventTimeoutProposeValue, data)
}
func (b *EventBus) PublishEventTimeoutWait(data types.EventDataRoundState) error {
return b.Publish(types.EventTimeoutWaitValue, data)
func (b *EventBus) PublishEventTimeoutWait(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventTimeoutWaitValue, data)
}
func (b *EventBus) PublishEventNewRound(data types.EventDataNewRound) error {
return b.Publish(types.EventNewRoundValue, data)
func (b *EventBus) PublishEventNewRound(ctx context.Context, data types.EventDataNewRound) error {
return b.Publish(ctx, types.EventNewRoundValue, data)
}
func (b *EventBus) PublishEventCompleteProposal(data types.EventDataCompleteProposal) error {
return b.Publish(types.EventCompleteProposalValue, data)
func (b *EventBus) PublishEventCompleteProposal(ctx context.Context, data types.EventDataCompleteProposal) error {
return b.Publish(ctx, types.EventCompleteProposalValue, data)
}
func (b *EventBus) PublishEventPolka(data types.EventDataRoundState) error {
return b.Publish(types.EventPolkaValue, data)
func (b *EventBus) PublishEventPolka(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventPolkaValue, data)
}
func (b *EventBus) PublishEventUnlock(data types.EventDataRoundState) error {
return b.Publish(types.EventUnlockValue, data)
func (b *EventBus) PublishEventUnlock(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventUnlockValue, data)
}
func (b *EventBus) PublishEventRelock(data types.EventDataRoundState) error {
return b.Publish(types.EventRelockValue, data)
func (b *EventBus) PublishEventRelock(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventRelockValue, data)
}
func (b *EventBus) PublishEventLock(data types.EventDataRoundState) error {
return b.Publish(types.EventLockValue, data)
func (b *EventBus) PublishEventLock(ctx context.Context, data types.EventDataRoundState) error {
return b.Publish(ctx, types.EventLockValue, data)
}
func (b *EventBus) PublishEventValidatorSetUpdates(data types.EventDataValidatorSetUpdates) error {
return b.Publish(types.EventValidatorSetUpdatesValue, data)
func (b *EventBus) PublishEventValidatorSetUpdates(ctx context.Context, data types.EventDataValidatorSetUpdates) error {
return b.Publish(ctx, types.EventValidatorSetUpdatesValue, data)
}
//-----------------------------------------------------------------------------
@@ -223,22 +209,22 @@ func (b *EventBus) PublishEventValidatorSetUpdates(data types.EventDataValidator
// NopEventBus implements a types.BlockEventPublisher that discards all events.
type NopEventBus struct{}
func (NopEventBus) PublishEventNewBlock(types.EventDataNewBlock) error {
func (NopEventBus) PublishEventNewBlock(context.Context, types.EventDataNewBlock) error {
return nil
}
func (NopEventBus) PublishEventNewBlockHeader(types.EventDataNewBlockHeader) error {
func (NopEventBus) PublishEventNewBlockHeader(context.Context, types.EventDataNewBlockHeader) error {
return nil
}
func (NopEventBus) PublishEventNewEvidence(types.EventDataNewEvidence) error {
func (NopEventBus) PublishEventNewEvidence(context.Context, types.EventDataNewEvidence) error {
return nil
}
func (NopEventBus) PublishEventTx(types.EventDataTx) error {
func (NopEventBus) PublishEventTx(context.Context, types.EventDataTx) error {
return nil
}
func (NopEventBus) PublishEventValidatorSetUpdates(types.EventDataValidatorSetUpdates) error {
func (NopEventBus) PublishEventValidatorSetUpdates(context.Context, types.EventDataValidatorSetUpdates) error {
return nil
}
+29 -33
View File
@@ -38,7 +38,7 @@ func TestEventBusPublishEventTx(t *testing.T) {
query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND testType.baz=1", tx.Hash())
txsSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -55,7 +55,7 @@ func TestEventBusPublishEventTx(t *testing.T) {
assert.Equal(t, result, edt.Result)
}()
err = eventBus.PublishEventTx(types.EventDataTx{
err = eventBus.PublishEventTx(ctx, types.EventDataTx{
TxResult: abci.TxResult{
Height: 1,
Index: 0,
@@ -96,7 +96,7 @@ func TestEventBusPublishEventNewBlock(t *testing.T) {
query := "tm.event='NewBlock' AND testType.baz=1 AND testType.foz=2"
blocksSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -113,7 +113,7 @@ func TestEventBusPublishEventNewBlock(t *testing.T) {
assert.Equal(t, resultEndBlock, edt.ResultEndBlock)
}()
err = eventBus.PublishEventNewBlock(types.EventDataNewBlock{
err = eventBus.PublishEventNewBlock(ctx, types.EventDataNewBlock{
Block: block,
BlockID: blockID,
ResultBeginBlock: resultBeginBlock,
@@ -205,7 +205,7 @@ func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
sub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: fmt.Sprintf("client-%d", i),
Query: tmquery.MustParse(tc.query),
Query: tmquery.MustCompile(tc.query),
})
require.NoError(t, err)
@@ -225,7 +225,7 @@ func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
}
}()
assert.NoError(t, eventBus.PublishEventTx(types.EventDataTx{
assert.NoError(t, eventBus.PublishEventTx(ctx, types.EventDataTx{
TxResult: abci.TxResult{
Height: 1,
Index: 0,
@@ -269,7 +269,7 @@ func TestEventBusPublishEventNewBlockHeader(t *testing.T) {
query := "tm.event='NewBlockHeader' AND testType.baz=1 AND testType.foz=2"
headersSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -285,7 +285,7 @@ func TestEventBusPublishEventNewBlockHeader(t *testing.T) {
assert.Equal(t, resultEndBlock, edt.ResultEndBlock)
}()
err = eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
err = eventBus.PublishEventNewBlockHeader(ctx, types.EventDataNewBlockHeader{
Header: block.Header,
ResultBeginBlock: resultBeginBlock,
ResultEndBlock: resultEndBlock,
@@ -312,7 +312,7 @@ func TestEventBusPublishEventNewEvidence(t *testing.T) {
const query = `tm.event='NewEvidence'`
evSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -327,7 +327,7 @@ func TestEventBusPublishEventNewEvidence(t *testing.T) {
assert.Equal(t, int64(4), edt.Height)
}()
err = eventBus.PublishEventNewEvidence(types.EventDataNewEvidence{
err = eventBus.PublishEventNewEvidence(ctx, types.EventDataNewEvidence{
Evidence: ev,
Height: 4,
})
@@ -352,7 +352,7 @@ func TestEventBusPublish(t *testing.T) {
sub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.Empty{},
Query: tmquery.All,
Limit: numEventsExpected,
})
require.NoError(t, err)
@@ -371,23 +371,23 @@ func TestEventBusPublish(t *testing.T) {
}
}()
require.NoError(t, eventBus.Publish(types.EventNewBlockHeaderValue,
require.NoError(t, eventBus.Publish(ctx, types.EventNewBlockHeaderValue,
types.EventDataNewBlockHeader{}))
require.NoError(t, eventBus.PublishEventNewBlock(types.EventDataNewBlock{}))
require.NoError(t, eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{}))
require.NoError(t, eventBus.PublishEventVote(types.EventDataVote{}))
require.NoError(t, eventBus.PublishEventNewRoundStep(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventTimeoutPropose(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventTimeoutWait(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventNewRound(types.EventDataNewRound{}))
require.NoError(t, eventBus.PublishEventCompleteProposal(types.EventDataCompleteProposal{}))
require.NoError(t, eventBus.PublishEventPolka(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventUnlock(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventRelock(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventLock(types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventValidatorSetUpdates(types.EventDataValidatorSetUpdates{}))
require.NoError(t, eventBus.PublishEventBlockSyncStatus(types.EventDataBlockSyncStatus{}))
require.NoError(t, eventBus.PublishEventStateSyncStatus(types.EventDataStateSyncStatus{}))
require.NoError(t, eventBus.PublishEventNewBlock(ctx, types.EventDataNewBlock{}))
require.NoError(t, eventBus.PublishEventNewBlockHeader(ctx, types.EventDataNewBlockHeader{}))
require.NoError(t, eventBus.PublishEventVote(ctx, types.EventDataVote{}))
require.NoError(t, eventBus.PublishEventNewRoundStep(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventTimeoutPropose(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventTimeoutWait(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventNewRound(ctx, types.EventDataNewRound{}))
require.NoError(t, eventBus.PublishEventCompleteProposal(ctx, types.EventDataCompleteProposal{}))
require.NoError(t, eventBus.PublishEventPolka(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventUnlock(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventRelock(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventLock(ctx, types.EventDataRoundState{}))
require.NoError(t, eventBus.PublishEventValidatorSetUpdates(ctx, types.EventDataValidatorSetUpdates{}))
require.NoError(t, eventBus.PublishEventBlockSyncStatus(ctx, types.EventDataBlockSyncStatus{}))
require.NoError(t, eventBus.PublishEventStateSyncStatus(ctx, types.EventDataStateSyncStatus{}))
require.GreaterOrEqual(t, <-count, numEventsExpected)
}
@@ -436,11 +436,7 @@ func benchmarkEventBus(numClients int, randQueries bool, randEvents bool, b *tes
if err != nil {
b.Error(err)
}
b.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
b.Error(err)
}
})
b.Cleanup(eventBus.Wait)
q := types.EventQueryNewBlock
@@ -473,7 +469,7 @@ func benchmarkEventBus(numClients int, randQueries bool, randEvents bool, b *tes
eventValue = randEventValue()
}
err := eventBus.Publish(eventValue, types.EventDataString("Gamora"))
err := eventBus.Publish(ctx, eventValue, types.EventDataString("Gamora"))
if err != nil {
b.Error(err)
}
-1
View File
@@ -509,7 +509,6 @@ func TestReactorBroadcastEvidence_FullyConnected(t *testing.T) {
}
}
// nolint:lll
func TestEvidenceListSerialization(t *testing.T) {
exampleVote := func(msgType byte) *types.Vote {
var stamp, err = time.Parse(types.TimeFormat, "2017-12-25T03:00:01.234Z")
-2
View File
@@ -41,8 +41,6 @@ type Inspector struct {
// 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 {
eb := eventbus.NewDefault(logger.With("module", "events"))
+1 -1
View File
@@ -115,7 +115,7 @@ func TestBlock(t *testing.T) {
func TestTxSearch(t *testing.T) {
testHash := []byte("test")
testTx := []byte("tx")
testQuery := fmt.Sprintf("tx.hash='%s'", string(testHash))
testQuery := fmt.Sprintf("tx.hash = '%s'", string(testHash))
testTxResult := &abcitypes.TxResult{
Height: 1,
Index: 100,
-2
View File
@@ -31,8 +31,6 @@ type eventBusUnsubscriber interface {
}
// 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,
+1 -5
View File
@@ -63,14 +63,10 @@ func main() {
for {
n, err := os.Stdin.Read(buf)
if err != nil {
if err := group.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "logjack stopped with error %v\n", headPath)
os.Exit(1)
}
if err == io.EOF {
os.Exit(0)
} else {
fmt.Println("logjack errored")
fmt.Println("logjack errored:", err.Error())
os.Exit(1)
}
}
+5 -4
View File
@@ -138,7 +138,7 @@ func GroupTotalSizeLimit(limit int64) func(*Group) {
// and group limits.
func (g *Group) OnStart(ctx context.Context) error {
g.ticker = time.NewTicker(g.groupCheckDuration)
go g.processTicks()
go g.processTicks(ctx)
return nil
}
@@ -237,15 +237,16 @@ func (g *Group) FlushAndSync() error {
return err
}
func (g *Group) processTicks() {
func (g *Group) processTicks(ctx context.Context) {
defer close(g.doneProcessTicks)
for {
select {
case <-ctx.Done():
return
case <-g.ticker.C:
g.checkHeadSizeLimit()
g.checkTotalSizeLimit()
case <-g.Quit():
return
}
}
}
+13 -11
View File
@@ -98,7 +98,9 @@ func setup(ctx context.Context, t testing.TB, cacheSize int, options ...TxMempoo
return NewTxMempool(logger.With("test", t.Name()), cfg.Mempool, appConnMem, 0, options...)
}
func checkTxs(t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx {
func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx {
t.Helper()
txs := make([]testTx, numTxs)
txInfo := TxInfo{SenderID: peerID}
@@ -115,7 +117,7 @@ func checkTxs(t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx
tx: []byte(fmt.Sprintf("sender-%d-%d=%X=%d", i, peerID, prefix, priority)),
priority: priority,
}
require.NoError(t, txmp.CheckTx(context.Background(), txs[i].tx, nil, txInfo))
require.NoError(t, txmp.CheckTx(ctx, txs[i].tx, nil, txInfo))
}
return txs
@@ -161,7 +163,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) {
// Execute CheckTx for some transactions and ensure TxsAvailable only fires
// once.
txs := checkTxs(t, txmp, 100, 0)
txs := checkTxs(ctx, t, txmp, 100, 0)
ensureTxFire()
ensureNoTxFire()
@@ -184,7 +186,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) {
// Execute CheckTx for more transactions and ensure we do not fire another
// event as we're still on the same height (1).
_ = checkTxs(t, txmp, 100, 0)
_ = checkTxs(ctx, t, txmp, 100, 0)
ensureNoTxFire()
}
@@ -193,7 +195,7 @@ func TestTxMempool_Size(t *testing.T) {
defer cancel()
txmp := setup(ctx, t, 0)
txs := checkTxs(t, txmp, 100, 0)
txs := checkTxs(ctx, t, txmp, 100, 0)
require.Equal(t, len(txs), txmp.Size())
require.Equal(t, int64(5690), txmp.SizeBytes())
@@ -220,7 +222,7 @@ func TestTxMempool_Flush(t *testing.T) {
defer cancel()
txmp := setup(ctx, t, 0)
txs := checkTxs(t, txmp, 100, 0)
txs := checkTxs(ctx, t, txmp, 100, 0)
require.Equal(t, len(txs), txmp.Size())
require.Equal(t, int64(5690), txmp.SizeBytes())
@@ -248,7 +250,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) {
defer cancel()
txmp := setup(ctx, t, 0)
tTxs := checkTxs(t, txmp, 100, 0) // all txs request 1 gas unit
tTxs := checkTxs(ctx, t, txmp, 100, 0) // all txs request 1 gas unit
require.Equal(t, len(tTxs), txmp.Size())
require.Equal(t, int64(5690), txmp.SizeBytes())
@@ -301,7 +303,7 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) {
defer cancel()
txmp := setup(ctx, t, 0)
tTxs := checkTxs(t, txmp, 100, 0)
tTxs := checkTxs(ctx, t, txmp, 100, 0)
require.Equal(t, len(tTxs), txmp.Size())
require.Equal(t, int64(5690), txmp.SizeBytes())
@@ -424,7 +426,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) {
wg.Add(1)
go func() {
for i := 0; i < 20; i++ {
_ = checkTxs(t, txmp, 100, 0)
_ = checkTxs(ctx, t, txmp, 100, 0)
dur := rng.Intn(1000-500) + 500
time.Sleep(time.Duration(dur) * time.Millisecond)
}
@@ -486,7 +488,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) {
txmp.height = 100
txmp.config.TTLNumBlocks = 10
tTxs := checkTxs(t, txmp, 100, 0)
tTxs := checkTxs(ctx, t, txmp, 100, 0)
require.Equal(t, len(tTxs), txmp.Size())
require.Equal(t, 100, txmp.heightIndex.Size())
@@ -505,7 +507,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) {
require.Equal(t, 95, txmp.heightIndex.Size())
// check more txs at height 101
_ = checkTxs(t, txmp, 50, 1)
_ = checkTxs(ctx, t, txmp, 50, 1)
require.Equal(t, 145, txmp.Size())
require.Equal(t, 145, txmp.heightIndex.Size())
+5 -5
View File
@@ -203,7 +203,7 @@ func TestReactorBroadcastTxs(t *testing.T) {
primary := rts.nodes[0]
secondaries := rts.nodes[1:]
txs := checkTxs(t, rts.reactors[primary].mempool, numTxs, UnknownPeerID)
txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, UnknownPeerID)
// run the router
rts.start(t)
@@ -238,7 +238,7 @@ func TestReactorConcurrency(t *testing.T) {
// 1. submit a bunch of txs
// 2. update the whole mempool
txs := checkTxs(t, rts.reactors[primary].mempool, numTxs, UnknownPeerID)
txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, UnknownPeerID)
go func() {
defer wg.Done()
@@ -257,7 +257,7 @@ func TestReactorConcurrency(t *testing.T) {
// 1. submit a bunch of txs
// 2. update none
_ = checkTxs(t, rts.reactors[secondary].mempool, numTxs, UnknownPeerID)
_ = checkTxs(ctx, t, rts.reactors[secondary].mempool, numTxs, UnknownPeerID)
go func() {
defer wg.Done()
@@ -290,7 +290,7 @@ func TestReactorNoBroadcastToSender(t *testing.T) {
secondary := rts.nodes[1]
peerID := uint16(1)
_ = checkTxs(t, rts.mempools[primary], numTxs, peerID)
_ = checkTxs(ctx, t, rts.mempools[primary], numTxs, peerID)
rts.start(t)
@@ -430,7 +430,7 @@ func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) {
}
time.Sleep(500 * time.Millisecond)
txs := checkTxs(t, rts.reactors[primary].mempool, 4, UnknownPeerID)
txs := checkTxs(ctx, t, rts.reactors[primary].mempool, 4, UnknownPeerID)
require.Equal(t, 4, len(txs))
require.Equal(t, 4, rts.mempools[primary].Size())
require.Equal(t, 0, rts.mempools[secondary].Size())
+4 -4
View File
@@ -101,6 +101,8 @@ type MConnection struct {
// are safe to call concurrently.
stopMtx tmsync.Mutex
cancel context.CancelFunc
flushTimer *timer.ThrottleTimer // flush writes as necessary but throttled.
pingTimer *time.Ticker // send pings periodically
@@ -187,6 +189,7 @@ func NewMConnectionWithConfig(
onError: onError,
config: config,
created: time.Now(),
cancel: func() {},
}
mconn.BaseService = *service.NewBaseService(logger, "MConnection", mconn)
@@ -211,9 +214,6 @@ func NewMConnectionWithConfig(
// OnStart implements BaseService
func (c *MConnection) OnStart(ctx context.Context) error {
if err := c.BaseService.OnStart(ctx); err != nil {
return err
}
c.flushTimer = timer.NewThrottleTimer("flush", c.config.FlushThrottle)
c.pingTimer = time.NewTicker(c.config.PingInterval)
c.pongTimeoutCh = make(chan bool, 1)
@@ -247,7 +247,6 @@ func (c *MConnection) stopServices() (alreadyStopped bool) {
default:
}
c.BaseService.OnStop()
c.flushTimer.Stop()
c.pingTimer.Stop()
c.chStatsTimer.Stop()
@@ -296,6 +295,7 @@ func (c *MConnection) stopForError(r interface{}) {
if err := c.Stop(); err != nil {
c.Logger.Error("Error stopping connection", "err", err)
}
if atomic.CompareAndSwapUint32(&c.errored, 0, 1) {
if c.onError != nil {
c.onError(r)
+35 -24
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"net"
"sync"
"testing"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/tendermint/tendermint/internal/libs/protoio"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/libs/service"
tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p"
"github.com/tendermint/tendermint/proto/tendermint/types"
)
@@ -54,7 +56,7 @@ func TestMConnectionSendFlushStop(t *testing.T) {
clientConn := createTestMConnection(log.TestingLogger(), client)
err := clientConn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, clientConn))
t.Cleanup(waitAll(clientConn))
msg := []byte("abc")
assert.True(t, clientConn.Send(0x01, msg))
@@ -91,7 +93,7 @@ func TestMConnectionSend(t *testing.T) {
mconn := createTestMConnection(log.TestingLogger(), client)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
msg := []byte("Ant-Man")
assert.True(t, mconn.Send(0x01, msg))
@@ -132,12 +134,12 @@ func TestMConnectionReceive(t *testing.T) {
mconn1 := createMConnectionWithCallbacks(logger, client, onReceive, onError)
err := mconn1.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn1))
t.Cleanup(waitAll(mconn1))
mconn2 := createTestMConnection(logger, server)
err = mconn2.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn2))
t.Cleanup(waitAll(mconn2))
msg := []byte("Cyclops")
assert.True(t, mconn2.Send(0x01, msg))
@@ -171,7 +173,7 @@ func TestMConnectionPongTimeoutResultsInError(t *testing.T) {
mconn := createMConnectionWithCallbacks(log.TestingLogger(), client, onReceive, onError)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
serverGotPing := make(chan struct{})
go func() {
@@ -212,7 +214,7 @@ func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) {
mconn := createMConnectionWithCallbacks(log.TestingLogger(), client, onReceive, onError)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
// sending 3 pongs in a row (abuse)
protoWriter := protoio.NewDelimitedWriter(server)
@@ -269,7 +271,7 @@ func TestMConnectionMultiplePings(t *testing.T) {
mconn := createMConnectionWithCallbacks(log.TestingLogger(), client, onReceive, onError)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
// sending 3 pings in a row (abuse)
// see https://github.com/tendermint/tendermint/issues/1190
@@ -320,7 +322,7 @@ func TestMConnectionPingPongs(t *testing.T) {
mconn := createMConnectionWithCallbacks(log.TestingLogger(), client, onReceive, onError)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
serverGotPing := make(chan struct{})
go func() {
@@ -380,7 +382,7 @@ func TestMConnectionStopsAndReturnsError(t *testing.T) {
mconn := createMConnectionWithCallbacks(log.TestingLogger(), client, onReceive, onError)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
if err := client.Close(); err != nil {
t.Error(err)
@@ -454,7 +456,7 @@ func TestMConnectionReadErrorBadEncoding(t *testing.T) {
_, err := client.Write([]byte{1, 2, 3, 4, 5})
require.NoError(t, err)
assert.True(t, expectSend(chOnErr), "badly encoded msgPacket")
t.Cleanup(stopAll(t, mconnClient, mconnServer))
t.Cleanup(waitAll(mconnClient, mconnServer))
}
func TestMConnectionReadErrorUnknownChannel(t *testing.T) {
@@ -473,7 +475,7 @@ func TestMConnectionReadErrorUnknownChannel(t *testing.T) {
// should cause an error
assert.True(t, mconnClient.Send(0x02, msg))
assert.True(t, expectSend(chOnErr), "unknown channel")
t.Cleanup(stopAll(t, mconnClient, mconnServer))
t.Cleanup(waitAll(mconnClient, mconnServer))
}
func TestMConnectionReadErrorLongMessage(t *testing.T) {
@@ -484,7 +486,7 @@ func TestMConnectionReadErrorLongMessage(t *testing.T) {
defer cancel()
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(ctx, t, chOnErr)
t.Cleanup(stopAll(t, mconnClient, mconnServer))
t.Cleanup(waitAll(mconnClient, mconnServer))
mconnServer.onReceive = func(chID ChannelID, msgBytes []byte) {
chOnRcv <- struct{}{}
@@ -522,7 +524,7 @@ func TestMConnectionReadErrorUnknownMsgType(t *testing.T) {
chOnErr := make(chan struct{})
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(ctx, t, chOnErr)
t.Cleanup(stopAll(t, mconnClient, mconnServer))
t.Cleanup(waitAll(mconnClient, mconnServer))
// send msg with unknown msg type
_, err := protoio.NewDelimitedWriter(mconnClient.conn).WriteMsg(&types.Header{ChainID: "x"})
@@ -539,7 +541,7 @@ func TestMConnectionTrySend(t *testing.T) {
mconn := createTestMConnection(log.TestingLogger(), client)
err := mconn.Start(ctx)
require.Nil(t, err)
t.Cleanup(stopAll(t, mconn))
t.Cleanup(waitAll(mconn))
msg := []byte("Semicolon-Woman")
resultCh := make(chan string, 2)
@@ -555,7 +557,6 @@ func TestMConnectionTrySend(t *testing.T) {
assert.Equal(t, "TrySend", <-resultCh)
}
// nolint:lll //ignore line length for tests
func TestConnVectors(t *testing.T) {
testCases := []struct {
@@ -587,7 +588,7 @@ func TestMConnectionChannelOverflow(t *testing.T) {
defer cancel()
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(ctx, t, chOnErr)
t.Cleanup(stopAll(t, mconnClient, mconnServer))
t.Cleanup(waitAll(mconnClient, mconnServer))
mconnServer.onReceive = func(chID ChannelID, msgBytes []byte) {
chOnRcv <- struct{}{}
@@ -612,16 +613,26 @@ func TestMConnectionChannelOverflow(t *testing.T) {
}
type stopper interface {
Stop() error
}
func stopAll(t *testing.T, stoppers ...stopper) func() {
func waitAll(waiters ...service.Service) func() {
return func() {
for _, s := range stoppers {
if err := s.Stop(); err != nil {
t.Log(err)
switch len(waiters) {
case 0:
return
case 1:
waiters[0].Wait()
return
default:
wg := &sync.WaitGroup{}
for _, w := range waiters {
wg.Add(1)
go func(s service.Service) {
defer wg.Done()
s.Wait()
}(w)
}
wg.Wait()
}
}
}
-2
View File
@@ -63,8 +63,6 @@ type Metrics struct {
// PrometheusMetrics returns Metrics build using Prometheus client library.
// Optionally, labels can be provided along with their values ("foo",
// "fooValue").
//
// nolint: lll
func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics {
labels := []string{}
for i := 0; i < len(labelsAndValues); i += 2 {
+13 -10
View File
@@ -141,8 +141,8 @@ func NewReactor(
// messages on that p2p channel accordingly. The caller must be sure to execute
// OnStop to ensure the outbound p2p Channels are closed.
func (r *Reactor) OnStart(ctx context.Context) error {
go r.processPexCh()
go r.processPeerUpdates()
go r.processPexCh(ctx)
go r.processPeerUpdates(ctx)
return nil
}
@@ -162,17 +162,22 @@ func (r *Reactor) OnStop() {
// processPexCh implements a blocking event loop where we listen for p2p
// Envelope messages from the pexCh.
func (r *Reactor) processPexCh() {
func (r *Reactor) processPexCh(ctx context.Context) {
defer r.pexCh.Close()
timer := time.NewTimer(0)
defer timer.Stop()
for {
timer.Reset(time.Until(r.nextRequestTime))
select {
case <-ctx.Done():
return
case <-r.closeCh:
r.Logger.Debug("stopped listening on PEX channel; closing...")
return
// outbound requests for new peers
case <-r.waitUntilNextRequest():
case <-timer.C:
r.sendRequestForPeers()
// inbound requests for new peers or responses to requests sent by this
@@ -192,11 +197,13 @@ func (r *Reactor) processPexCh() {
// processPeerUpdates initiates a blocking process where we listen for and handle
// PeerUpdate messages. When the reactor is stopped, we will catch the signal and
// close the p2p PeerUpdatesCh gracefully.
func (r *Reactor) processPeerUpdates() {
func (r *Reactor) processPeerUpdates(ctx context.Context) {
defer r.peerUpdates.Close()
for {
select {
case <-ctx.Done():
return
case peerUpdate := <-r.peerUpdates.Updates():
r.processPeerUpdate(peerUpdate)
@@ -317,10 +324,6 @@ func (r *Reactor) processPeerUpdate(peerUpdate p2p.PeerUpdate) {
}
}
func (r *Reactor) waitUntilNextRequest() <-chan time.Time {
return time.After(time.Until(r.nextRequestTime))
}
// sendRequestForPeers pops the first peerID off the list and sends the
// peer a request for more peer addresses. The function then moves the
// peer into the requestsSent bucket and calculates when the next request
-55
View File
@@ -1,55 +0,0 @@
package trust
import "time"
// MetricConfig - Configures the weight functions and time intervals for the metric
type MetricConfig struct {
// Determines the percentage given to current behavior
ProportionalWeight float64
// Determines the percentage given to prior behavior
IntegralWeight float64
// The window of time that the trust metric will track events across.
// This can be set to cover many days without issue
TrackingWindow time.Duration
// Each interval should be short for adapability.
// Less than 30 seconds is too sensitive,
// and greater than 5 minutes will make the metric numb
IntervalLength time.Duration
}
// DefaultConfig returns a config with values that have been tested and produce desirable results
func DefaultConfig() MetricConfig {
return MetricConfig{
ProportionalWeight: 0.4,
IntegralWeight: 0.6,
TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days.
IntervalLength: 1 * time.Minute,
}
}
// Ensures that all configuration elements have valid values
func customConfig(tmc MetricConfig) MetricConfig {
config := DefaultConfig()
// Check the config for set values, and setup appropriately
if tmc.ProportionalWeight > 0 {
config.ProportionalWeight = tmc.ProportionalWeight
}
if tmc.IntegralWeight > 0 {
config.IntegralWeight = tmc.IntegralWeight
}
if tmc.IntervalLength > time.Duration(0) {
config.IntervalLength = tmc.IntervalLength
}
if tmc.TrackingWindow > time.Duration(0) &&
tmc.TrackingWindow >= config.IntervalLength {
config.TrackingWindow = tmc.TrackingWindow
}
return config
}
-413
View File
@@ -1,413 +0,0 @@
// Copyright 2017 Tendermint. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package trust
import (
"context"
"math"
"time"
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
"github.com/tendermint/tendermint/libs/service"
)
//---------------------------------------------------------------------------------------
const (
// The weight applied to the derivative when current behavior is >= previous behavior
defaultDerivativeGamma1 = 0
// The weight applied to the derivative when current behavior is less than previous behavior
defaultDerivativeGamma2 = 1.0
// The weight applied to history data values when calculating the history value
defaultHistoryDataWeight = 0.8
)
// MetricHistoryJSON - history data necessary to save the trust metric
type MetricHistoryJSON struct {
NumIntervals int `json:"intervals"`
History []float64 `json:"history"`
}
// Metric - keeps track of peer reliability
// See tendermint/docs/architecture/adr-006-trust-metric.md for details
type Metric struct {
service.BaseService
// Mutex that protects the metric from concurrent access
mtx tmsync.Mutex
// Determines the percentage given to current behavior
proportionalWeight float64
// Determines the percentage given to prior behavior
integralWeight float64
// Count of how many time intervals this metric has been tracking
numIntervals int
// Size of the time interval window for this trust metric
maxIntervals int
// The time duration for a single time interval
intervalLen time.Duration
// Stores the trust history data for this metric
history []float64
// Weights applied to the history data when calculating the history value
historyWeights []float64
// The sum of the history weights used when calculating the history value
historyWeightSum float64
// The current number of history data elements
historySize int
// The maximum number of history data elements
historyMaxSize int
// The calculated history value for the current time interval
historyValue float64
// The number of recorded good and bad events for the current time interval
bad, good float64
// While true, history data is not modified
paused bool
// Used during testing in order to control the passing of time intervals
testTicker MetricTicker
}
// NewMetric returns a trust metric with the default configuration.
// Use Start to begin tracking the quality of peer behavior over time
func NewMetric() *Metric {
return NewMetricWithConfig(DefaultConfig())
}
// NewMetricWithConfig returns a trust metric with a custom configuration.
// Use Start to begin tracking the quality of peer behavior over time
func NewMetricWithConfig(tmc MetricConfig) *Metric {
tm := new(Metric)
config := customConfig(tmc)
// Setup using the configuration values
tm.proportionalWeight = config.ProportionalWeight
tm.integralWeight = config.IntegralWeight
tm.intervalLen = config.IntervalLength
// The maximum number of time intervals is the tracking window / interval length
tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen)
// The history size will be determined by the maximum number of time intervals
tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1
// This metric has a perfect history so far
tm.historyValue = 1.0
tm.BaseService = *service.NewBaseService(nil, "Metric", tm)
return tm
}
// OnStart implements Service
func (tm *Metric) OnStart(ctx context.Context) error {
if err := tm.BaseService.OnStart(ctx); err != nil {
return err
}
go tm.processRequests()
return nil
}
// OnStop implements Service
// Nothing to do since the goroutine shuts down by itself via BaseService.Quit()
func (tm *Metric) OnStop() {}
// Returns a snapshot of the trust metric history data
func (tm *Metric) HistoryJSON() MetricHistoryJSON {
tm.mtx.Lock()
defer tm.mtx.Unlock()
return MetricHistoryJSON{
NumIntervals: tm.numIntervals,
History: tm.history,
}
}
// Instantiates a trust metric by loading the history data for a single peer.
// This is called only once and only right after creation, which is why the
// lock is not held while accessing the trust metric struct members
func (tm *Metric) Init(hist MetricHistoryJSON) {
// Restore the number of time intervals we have previously tracked
if hist.NumIntervals > tm.maxIntervals {
hist.NumIntervals = tm.maxIntervals
}
tm.numIntervals = hist.NumIntervals
// Restore the history and its current size
if len(hist.History) > tm.historyMaxSize {
// Keep the history no larger than historyMaxSize
last := len(hist.History) - tm.historyMaxSize
hist.History = hist.History[last:]
}
tm.history = hist.History
tm.historySize = len(tm.history)
// Create the history weight values and weight sum
for i := 1; i <= tm.numIntervals; i++ {
x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight
tm.historyWeights = append(tm.historyWeights, x)
}
for _, v := range tm.historyWeights {
tm.historyWeightSum += v
}
// Calculate the history value based on the loaded history data
tm.historyValue = tm.calcHistoryValue()
}
// Pause tells the metric to pause recording data over time intervals.
// All method calls that indicate events will unpause the metric
func (tm *Metric) Pause() {
tm.mtx.Lock()
defer tm.mtx.Unlock()
// Pause the metric for now
tm.paused = true
}
// BadEvents indicates that an undesirable event(s) took place
func (tm *Metric) BadEvents(num int) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.unpause()
tm.bad += float64(num)
}
// GoodEvents indicates that a desirable event(s) took place
func (tm *Metric) GoodEvents(num int) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.unpause()
tm.good += float64(num)
}
// TrustValue gets the dependable trust value; always between 0 and 1
func (tm *Metric) TrustValue() float64 {
tm.mtx.Lock()
defer tm.mtx.Unlock()
return tm.calcTrustValue()
}
// TrustScore gets a score based on the trust value always between 0 and 100
func (tm *Metric) TrustScore() int {
score := tm.TrustValue() * 100
return int(math.Floor(score))
}
// NextTimeInterval saves current time interval data and prepares for the following interval
func (tm *Metric) NextTimeInterval() {
tm.mtx.Lock()
defer tm.mtx.Unlock()
if tm.paused {
// Do not prepare for the next time interval while paused
return
}
// Add the current trust value to the history data
newHist := tm.calcTrustValue()
tm.history = append(tm.history, newHist)
// Update history and interval counters
if tm.historySize < tm.historyMaxSize {
tm.historySize++
} else {
// Keep the history no larger than historyMaxSize
last := len(tm.history) - tm.historyMaxSize
tm.history = tm.history[last:]
}
if tm.numIntervals < tm.maxIntervals {
tm.numIntervals++
// Add the optimistic weight for the new time interval
wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals))
tm.historyWeights = append(tm.historyWeights, wk)
tm.historyWeightSum += wk
}
// Update the history data using Faded Memories
tm.updateFadedMemory()
// Calculate the history value for the upcoming time interval
tm.historyValue = tm.calcHistoryValue()
tm.good = 0
tm.bad = 0
}
// SetTicker allows a TestTicker to be provided that will manually control
// the passing of time from the perspective of the Metric.
// The ticker must be set before Start is called on the metric
func (tm *Metric) SetTicker(ticker MetricTicker) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.testTicker = ticker
}
// Copy returns a new trust metric with members containing the same values
func (tm *Metric) Copy() *Metric {
if tm == nil {
return nil
}
tm.mtx.Lock()
defer tm.mtx.Unlock()
return &Metric{
proportionalWeight: tm.proportionalWeight,
integralWeight: tm.integralWeight,
numIntervals: tm.numIntervals,
maxIntervals: tm.maxIntervals,
intervalLen: tm.intervalLen,
history: tm.history,
historyWeights: tm.historyWeights,
historyWeightSum: tm.historyWeightSum,
historySize: tm.historySize,
historyMaxSize: tm.historyMaxSize,
historyValue: tm.historyValue,
good: tm.good,
bad: tm.bad,
paused: tm.paused,
}
}
/* Private methods */
// This method is for a goroutine that handles all requests on the metric
func (tm *Metric) processRequests() {
t := tm.testTicker
if t == nil {
// No test ticker was provided, so we create a normal ticker
t = NewTicker(tm.intervalLen)
}
defer t.Stop()
// Obtain the raw channel
tick := t.GetChannel()
loop:
for {
select {
case <-tick:
tm.NextTimeInterval()
case <-tm.Quit():
// Stop all further tracking for this metric
break loop
}
}
}
// Wakes the trust metric up if it is currently paused
// This method needs to be called with the mutex locked
func (tm *Metric) unpause() {
// Check if this is the first experience with
// what we are tracking since being paused
if tm.paused {
tm.good = 0
tm.bad = 0
// New events cause us to unpause the metric
tm.paused = false
}
}
// Calculates the trust value for the request processing
func (tm *Metric) calcTrustValue() float64 {
weightedP := tm.proportionalWeight * tm.proportionalValue()
weightedI := tm.integralWeight * tm.historyValue
weightedD := tm.weightedDerivative()
tv := weightedP + weightedI + weightedD
// Do not return a negative value.
if tv < 0 {
tv = 0
}
return tv
}
// Calculates the current score for good/bad experiences
func (tm *Metric) proportionalValue() float64 {
value := 1.0
total := tm.good + tm.bad
if total > 0 {
value = tm.good / total
}
return value
}
// Strengthens the derivative component when the change is negative
func (tm *Metric) weightedDerivative() float64 {
var weight float64 = defaultDerivativeGamma1
d := tm.derivativeValue()
if d < 0 {
weight = defaultDerivativeGamma2
}
return weight * d
}
// Calculates the derivative component
func (tm *Metric) derivativeValue() float64 {
return tm.proportionalValue() - tm.historyValue
}
// Calculates the integral (history) component of the trust value
func (tm *Metric) calcHistoryValue() float64 {
var hv float64
for i := 0; i < tm.numIntervals; i++ {
hv += tm.fadedMemoryValue(i) * tm.historyWeights[i]
}
return hv / tm.historyWeightSum
}
// Retrieves the actual history data value that represents the requested time interval
func (tm *Metric) fadedMemoryValue(interval int) float64 {
first := tm.historySize - 1
if interval == 0 {
// Base case
return tm.history[first]
}
offset := intervalToHistoryOffset(interval)
return tm.history[first-offset]
}
// Performs the update for our Faded Memories process, which allows the
// trust metric tracking window to be large while maintaining a small
// number of history data values
func (tm *Metric) updateFadedMemory() {
if tm.historySize < 2 {
return
}
end := tm.historySize - 1
// Keep the most recent history element
for count := 1; count < tm.historySize; count++ {
i := end - count
// The older the data is, the more we spread it out
x := math.Pow(2, float64(count))
// Two history data values are merged into a single value
tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x
}
}
// Map the interval value down to an offset from the beginning of history
func intervalToHistoryOffset(interval int) int {
// The system maintains 2^m interval values in the form of m history
// data values. Therefore, we access the ith interval by obtaining
// the history data index = the floor of log2(i)
return int(math.Floor(math.Log2(float64(interval))))
}
-128
View File
@@ -1,128 +0,0 @@
package trust
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTrustMetricScores(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tm := NewMetric()
err := tm.Start(ctx)
require.NoError(t, err)
// Perfect score
tm.GoodEvents(1)
score := tm.TrustScore()
assert.Equal(t, 100, score)
// Less than perfect score
tm.BadEvents(10)
score = tm.TrustScore()
assert.NotEqual(t, 100, score)
err = tm.Stop()
require.NoError(t, err)
}
func TestTrustMetricConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 7 days
window := time.Minute * 60 * 24 * 7
config := MetricConfig{
TrackingWindow: window,
IntervalLength: 2 * time.Minute,
}
tm := NewMetricWithConfig(config)
err := tm.Start(ctx)
require.NoError(t, err)
// The max time intervals should be the TrackingWindow / IntervalLen
assert.Equal(t, int(config.TrackingWindow/config.IntervalLength), tm.maxIntervals)
dc := DefaultConfig()
// These weights should still be the default values
assert.Equal(t, dc.ProportionalWeight, tm.proportionalWeight)
assert.Equal(t, dc.IntegralWeight, tm.integralWeight)
err = tm.Stop()
require.NoError(t, err)
tm.Wait()
config.ProportionalWeight = 0.3
config.IntegralWeight = 0.7
tm = NewMetricWithConfig(config)
err = tm.Start(ctx)
require.NoError(t, err)
// These weights should be equal to our custom values
assert.Equal(t, config.ProportionalWeight, tm.proportionalWeight)
assert.Equal(t, config.IntegralWeight, tm.integralWeight)
err = tm.Stop()
require.NoError(t, err)
tm.Wait()
}
func TestTrustMetricCopyNilPointer(t *testing.T) {
var tm *Metric
ctm := tm.Copy()
assert.Nil(t, ctm)
}
// XXX: This test fails non-deterministically
//nolint:unused,deadcode
func _TestTrustMetricStopPause(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// The TestTicker will provide manual control over
// the passing of time within the metric
tt := NewTestTicker()
tm := NewMetric()
tm.SetTicker(tt)
err := tm.Start(ctx)
require.NoError(t, err)
// Allow some time intervals to pass and pause
tt.NextTick()
tt.NextTick()
tm.Pause()
// could be 1 or 2 because Pause and NextTick race
first := tm.Copy().numIntervals
// Allow more time to pass and check the intervals are unchanged
tt.NextTick()
tt.NextTick()
assert.Equal(t, first, tm.Copy().numIntervals)
// Get the trust metric activated again
tm.GoodEvents(5)
// Allow some time intervals to pass and stop
tt.NextTick()
tt.NextTick()
err = tm.Stop()
require.NoError(t, err)
tm.Wait()
second := tm.Copy().numIntervals
// Allow more intervals to pass while the metric is stopped
// and check that the number of intervals match
tm.NextTimeInterval()
tm.NextTimeInterval()
// XXX: fails non-deterministically:
// expected 5, got 6
assert.Equal(t, second+2, tm.Copy().numIntervals)
if first > second {
t.Fatalf("numIntervals should always increase or stay the same over time")
}
}
-222
View File
@@ -1,222 +0,0 @@
// Copyright 2017 Tendermint. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package trust
import (
"context"
"encoding/json"
"fmt"
"time"
dbm "github.com/tendermint/tm-db"
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/libs/service"
)
const defaultStorePeriodicSaveInterval = 1 * time.Minute
var trustMetricKey = []byte("trustMetricStore")
// MetricStore - Manages all trust metrics for peers
type MetricStore struct {
service.BaseService
// Maps a Peer.Key to that peer's TrustMetric
peerMetrics map[string]*Metric
// Mutex that protects the map and history data file
mtx tmsync.Mutex
// The db where peer trust metric history data will be stored
db dbm.DB
// This configuration will be used when creating new TrustMetrics
config MetricConfig
}
// NewTrustMetricStore returns a store that saves data to the DB
// and uses the config when creating new trust metrics.
// Use Start to to initialize the trust metric store
func NewTrustMetricStore(db dbm.DB, tmc MetricConfig, logger log.Logger) *MetricStore {
tms := &MetricStore{
peerMetrics: make(map[string]*Metric),
db: db,
config: tmc,
}
tms.BaseService = *service.NewBaseService(logger, "MetricStore", tms)
return tms
}
// OnStart implements Service
func (tms *MetricStore) OnStart(ctx context.Context) error {
if err := tms.BaseService.OnStart(ctx); err != nil {
return err
}
tms.mtx.Lock()
defer tms.mtx.Unlock()
tms.loadFromDB(ctx)
go tms.saveRoutine()
return nil
}
// OnStop implements Service
func (tms *MetricStore) OnStop() {
tms.BaseService.OnStop()
tms.mtx.Lock()
defer tms.mtx.Unlock()
// Stop all trust metric go-routines
for _, tm := range tms.peerMetrics {
if err := tm.Stop(); err != nil {
tms.Logger.Error("unable to stop metric store", "error", err)
}
}
// Make the final trust history data save
tms.saveToDB()
}
// Size returns the number of entries in the trust metric store
func (tms *MetricStore) Size() int {
tms.mtx.Lock()
defer tms.mtx.Unlock()
return tms.size()
}
// AddPeerTrustMetric takes an existing trust metric and associates it with a peer key.
// The caller is expected to call Start on the TrustMetric being added
func (tms *MetricStore) AddPeerTrustMetric(key string, tm *Metric) {
tms.mtx.Lock()
defer tms.mtx.Unlock()
if key == "" || tm == nil {
return
}
tms.peerMetrics[key] = tm
}
// GetPeerTrustMetric returns a trust metric by peer key
func (tms *MetricStore) GetPeerTrustMetric(ctx context.Context, key string) *Metric {
tms.mtx.Lock()
defer tms.mtx.Unlock()
tm, ok := tms.peerMetrics[key]
if !ok {
// If the metric is not available, we will create it
tm = NewMetricWithConfig(tms.config)
if err := tm.Start(ctx); err != nil {
tms.Logger.Error("unable to start metric store", "error", err)
}
// The metric needs to be in the map
tms.peerMetrics[key] = tm
}
return tm
}
// PeerDisconnected pauses the trust metric associated with the peer identified by the key
func (tms *MetricStore) PeerDisconnected(key string) {
tms.mtx.Lock()
defer tms.mtx.Unlock()
// If the Peer that disconnected has a metric, pause it
if tm, ok := tms.peerMetrics[key]; ok {
tm.Pause()
}
}
// Saves the history data for all peers to the store DB.
// This public method acquires the trust metric store lock
func (tms *MetricStore) SaveToDB() {
tms.mtx.Lock()
defer tms.mtx.Unlock()
tms.saveToDB()
}
/* Private methods */
// size returns the number of entries in the store without acquiring the mutex
func (tms *MetricStore) size() int {
return len(tms.peerMetrics)
}
/* Loading & Saving */
/* Both loadFromDB and savetoDB assume the mutex has been acquired */
// Loads the history data for all peers from the store DB
// cmn.Panics if file is corrupt
func (tms *MetricStore) loadFromDB(ctx context.Context) bool {
// Obtain the history data we have so far
bytes, err := tms.db.Get(trustMetricKey)
if err != nil {
panic(err)
}
if bytes == nil {
return false
}
peers := make(map[string]MetricHistoryJSON)
err = json.Unmarshal(bytes, &peers)
if err != nil {
panic(fmt.Sprintf("Could not unmarshal Trust Metric Store DB data: %v", err))
}
// If history data exists in the file,
// load it into trust metric
for key, p := range peers {
tm := NewMetricWithConfig(tms.config)
if err := tm.Start(ctx); err != nil {
tms.Logger.Error("unable to start metric", "error", err)
}
tm.Init(p)
// Load the peer trust metric into the store
tms.peerMetrics[key] = tm
}
return true
}
// Saves the history data for all peers to the store DB
func (tms *MetricStore) saveToDB() {
tms.Logger.Debug("Saving TrustHistory to DB", "size", tms.size())
peers := make(map[string]MetricHistoryJSON)
for key, tm := range tms.peerMetrics {
// Add an entry for the peer identified by key
peers[key] = tm.HistoryJSON()
}
// Write all the data back to the DB
bytes, err := json.Marshal(peers)
if err != nil {
tms.Logger.Error("Failed to encode the TrustHistory", "err", err)
return
}
if err := tms.db.SetSync(trustMetricKey, bytes); err != nil {
tms.Logger.Error("failed to flush data to disk", "error", err)
}
}
// Periodically saves the trust history data to the DB
func (tms *MetricStore) saveRoutine() {
t := time.NewTicker(defaultStorePeriodicSaveInterval)
defer t.Stop()
loop:
for {
select {
case <-t.C:
tms.SaveToDB()
case <-tms.Quit():
break loop
}
}
}
-176
View File
@@ -1,176 +0,0 @@
// Copyright 2017 Tendermint. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package trust
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/libs/log"
)
func TestTrustMetricStoreSaveLoad(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dir := t.TempDir()
logger := log.TestingLogger()
historyDB, err := dbm.NewDB("trusthistory", "goleveldb", dir)
require.NoError(t, err)
// 0 peers saved
store := NewTrustMetricStore(historyDB, DefaultConfig(), logger)
store.saveToDB()
// Load the data from the file
store = NewTrustMetricStore(historyDB, DefaultConfig(), logger)
err = store.Start(ctx)
require.NoError(t, err)
// Make sure we still have 0 entries
assert.Zero(t, store.Size())
// 100 TestTickers
var tt []*TestTicker
for i := 0; i < 100; i++ {
// The TestTicker will provide manual control over
// the passing of time within the metric
tt = append(tt, NewTestTicker())
}
// 100 peers
for i := 0; i < 100; i++ {
key := fmt.Sprintf("peer_%d", i)
tm := NewMetric()
tm.SetTicker(tt[i])
err = tm.Start(ctx)
require.NoError(t, err)
store.AddPeerTrustMetric(key, tm)
tm.BadEvents(10)
tm.GoodEvents(1)
}
// Check that we have 100 entries and save
assert.Equal(t, 100, store.Size())
// Give the 100 metrics time to process the history data
for i := 0; i < 100; i++ {
tt[i].NextTick()
tt[i].NextTick()
}
// Stop all the trust metrics and save
err = store.Stop()
require.NoError(t, err)
// Load the data from the DB
store = NewTrustMetricStore(historyDB, DefaultConfig(), logger)
err = store.Start(ctx)
require.NoError(t, err)
// Check that we still have 100 peers with imperfect trust values
assert.Equal(t, 100, store.Size())
for _, tm := range store.peerMetrics {
assert.NotEqual(t, 1.0, tm.TrustValue())
}
err = store.Stop()
require.NoError(t, err)
}
func TestTrustMetricStoreConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
historyDB, err := dbm.NewDB("", "memdb", "")
require.NoError(t, err)
config := MetricConfig{
ProportionalWeight: 0.5,
IntegralWeight: 0.5,
}
logger := log.TestingLogger()
// Create a store with custom config
store := NewTrustMetricStore(historyDB, config, logger)
err = store.Start(ctx)
require.NoError(t, err)
// Have the store make us a metric with the config
tm := store.GetPeerTrustMetric(ctx, "TestKey")
// Check that the options made it to the metric
assert.Equal(t, 0.5, tm.proportionalWeight)
assert.Equal(t, 0.5, tm.integralWeight)
err = store.Stop()
require.NoError(t, err)
}
func TestTrustMetricStoreLookup(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
historyDB, err := dbm.NewDB("", "memdb", "")
require.NoError(t, err)
store := NewTrustMetricStore(historyDB, DefaultConfig(), log.TestingLogger())
err = store.Start(ctx)
require.NoError(t, err)
// Create 100 peers in the trust metric store
for i := 0; i < 100; i++ {
key := fmt.Sprintf("peer_%d", i)
store.GetPeerTrustMetric(ctx, key)
// Check that the trust metric was successfully entered
ktm := store.peerMetrics[key]
assert.NotNil(t, ktm, "Expected to find TrustMetric %s but wasn't there.", key)
}
err = store.Stop()
require.NoError(t, err)
}
func TestTrustMetricStorePeerScore(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
historyDB, err := dbm.NewDB("", "memdb", "")
require.NoError(t, err)
store := NewTrustMetricStore(historyDB, DefaultConfig(), log.TestingLogger())
err = store.Start(ctx)
require.NoError(t, err)
key := "TestKey"
tm := store.GetPeerTrustMetric(ctx, key)
// This peer is innocent so far
first := tm.TrustScore()
assert.Equal(t, 100, first)
// Add some undesirable events and disconnect
tm.BadEvents(1)
first = tm.TrustScore()
assert.NotEqual(t, 100, first)
tm.BadEvents(10)
second := tm.TrustScore()
if second > first {
t.Errorf("a greater number of bad events should lower the trust score")
}
store.PeerDisconnected(key)
// We will remember our experiences with this peer
tm = store.GetPeerTrustMetric(ctx, key)
assert.NotEqual(t, 100, tm.TrustScore())
err = store.Stop()
require.NoError(t, err)
}
-62
View File
@@ -1,62 +0,0 @@
// Copyright 2017 Tendermint. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package trust
import (
"time"
)
// MetricTicker provides a single ticker interface for the trust metric
type MetricTicker interface {
// GetChannel returns the receive only channel that fires at each time interval
GetChannel() <-chan time.Time
// Stop will halt further activity on the ticker channel
Stop()
}
// The ticker used during testing that provides manual control over time intervals
type TestTicker struct {
C chan time.Time
stopped bool
}
// NewTestTicker returns our ticker used within test routines
func NewTestTicker() *TestTicker {
c := make(chan time.Time)
return &TestTicker{
C: c,
}
}
func (t *TestTicker) GetChannel() <-chan time.Time {
return t.C
}
func (t *TestTicker) Stop() {
t.stopped = true
}
// NextInterval manually sends Time on the ticker channel
func (t *TestTicker) NextTick() {
if t.stopped {
return
}
t.C <- time.Now()
}
// Ticker is just a wrap around time.Ticker that allows it
// to meet the requirements of our interface
type Ticker struct {
*time.Ticker
}
// NewTicker returns a normal time.Ticker wrapped to meet our interface
func NewTicker(d time.Duration) *Ticker {
return &Ticker{time.NewTicker(d)}
}
func (t *Ticker) GetChannel() <-chan time.Time {
return t.C
}
+1 -1
View File
@@ -59,7 +59,7 @@ func (env *Environment) BroadcastTxSync(ctx *rpctypes.Context, tx types.Tx) (*co
// BroadcastTxCommit returns with the responses from CheckTx and DeliverTx.
// More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit
func (env *Environment) BroadcastTxCommit(ctx *rpctypes.Context, tx types.Tx) (*coretypes.ResultBroadcastTxCommit, error) { //nolint:lll
func (env *Environment) BroadcastTxCommit(ctx *rpctypes.Context, tx types.Tx) (*coretypes.ResultBroadcastTxCommit, error) {
resCh := make(chan *abci.Response, 1)
err := env.Mempool.CheckTx(
ctx.Context(),
+13 -8
View File
@@ -150,7 +150,10 @@ func (blockExec *BlockExecutor) ValidateBlock(state State, block *types.Block) e
// from outside this package to process and commit an entire block.
// It takes a blockID to avoid recomputing the parts hash.
func (blockExec *BlockExecutor) ApplyBlock(
state State, blockID types.BlockID, block *types.Block,
ctx context.Context,
state State,
blockID types.BlockID,
block *types.Block,
) (State, error) {
// validate the block if we haven't already
@@ -232,7 +235,7 @@ func (blockExec *BlockExecutor) ApplyBlock(
// Events are fired after everything else.
// NOTE: if we crash between Commit and Save, events wont be fired during replay
fireEvents(blockExec.logger, blockExec.eventBus, block, blockID, abciResponses, validatorUpdates)
fireEvents(ctx, blockExec.logger, blockExec.eventBus, block, blockID, abciResponses, validatorUpdates)
return state, nil
}
@@ -508,6 +511,7 @@ func updateState(
// Fire TxEvent for every tx.
// NOTE: if Tendermint crashes before commit, some or all of these events may be published again.
func fireEvents(
ctx context.Context,
logger log.Logger,
eventBus types.BlockEventPublisher,
block *types.Block,
@@ -515,7 +519,7 @@ func fireEvents(
abciResponses *tmstate.ABCIResponses,
validatorUpdates []*types.Validator,
) {
if err := eventBus.PublishEventNewBlock(types.EventDataNewBlock{
if err := eventBus.PublishEventNewBlock(ctx, types.EventDataNewBlock{
Block: block,
BlockID: blockID,
ResultBeginBlock: *abciResponses.BeginBlock,
@@ -524,7 +528,7 @@ func fireEvents(
logger.Error("failed publishing new block", "err", err)
}
if err := eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
if err := eventBus.PublishEventNewBlockHeader(ctx, types.EventDataNewBlockHeader{
Header: block.Header,
NumTxs: int64(len(block.Txs)),
ResultBeginBlock: *abciResponses.BeginBlock,
@@ -535,7 +539,7 @@ func fireEvents(
if len(block.Evidence.Evidence) != 0 {
for _, ev := range block.Evidence.Evidence {
if err := eventBus.PublishEventNewEvidence(types.EventDataNewEvidence{
if err := eventBus.PublishEventNewEvidence(ctx, types.EventDataNewEvidence{
Evidence: ev,
Height: block.Height,
}); err != nil {
@@ -545,7 +549,7 @@ func fireEvents(
}
for i, tx := range block.Data.Txs {
if err := eventBus.PublishEventTx(types.EventDataTx{TxResult: abci.TxResult{
if err := eventBus.PublishEventTx(ctx, types.EventDataTx{TxResult: abci.TxResult{
Height: block.Height,
Index: uint32(i),
Tx: tx,
@@ -556,7 +560,7 @@ func fireEvents(
}
if len(validatorUpdates) > 0 {
if err := eventBus.PublishEventValidatorSetUpdates(
if err := eventBus.PublishEventValidatorSetUpdates(ctx,
types.EventDataValidatorSetUpdates{ValidatorUpdates: validatorUpdates}); err != nil {
logger.Error("failed publishing event", "err", err)
}
@@ -569,6 +573,7 @@ func fireEvents(
// ExecCommitBlock executes and commits a block on the proxyApp without validating or mutating the state.
// It returns the application root hash (result of abci.Commit).
func ExecCommitBlock(
ctx context.Context,
be *BlockExecutor,
appConnConsensus proxy.AppConnConsensus,
block *types.Block,
@@ -598,7 +603,7 @@ func ExecCommitBlock(
}
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(types.BlockPartSizeBytes).Header()}
fireEvents(be.logger, be.eventBus, block, blockID, abciResponses, validatorUpdates)
fireEvents(ctx, be.logger, be.eventBus, block, blockID, abciResponses, validatorUpdates)
}
// Commit block, get hash back
+5 -5
View File
@@ -56,7 +56,7 @@ func TestApplyBlock(t *testing.T) {
block := sf.MakeBlock(state, 1, new(types.Commit))
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
state, err = blockExec.ApplyBlock(state, blockID, block)
state, err = blockExec.ApplyBlock(ctx, state, blockID, block)
require.Nil(t, err)
// TODO check state and mempool
@@ -111,7 +111,7 @@ func TestBeginBlockValidators(t *testing.T) {
// block for height 2
block := sf.MakeBlock(state, 2, lastCommit)
_, err = sm.ExecCommitBlock(nil, proxyApp.Consensus(), block, log.TestingLogger(), stateStore, 1, state)
_, err = sm.ExecCommitBlock(ctx, nil, proxyApp.Consensus(), block, log.TestingLogger(), stateStore, 1, state)
require.Nil(t, err, tc.desc)
// -> app receives a list of validators with a bool indicating if they signed
@@ -219,7 +219,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) {
block.Header.EvidenceHash = block.Evidence.Hash()
blockID = types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
_, err = blockExec.ApplyBlock(state, blockID, block)
_, err = blockExec.ApplyBlock(ctx, state, blockID, block)
require.Nil(t, err)
// TODO check state and mempool
@@ -404,7 +404,7 @@ func TestEndBlockValidatorUpdates(t *testing.T) {
{PubKey: pk, Power: 10},
}
state, err = blockExec.ApplyBlock(state, blockID, block)
state, err = blockExec.ApplyBlock(ctx, state, blockID, block)
require.Nil(t, err)
// test new validator was added to NextValidators
if assert.Equal(t, state.Validators.Size()+1, state.NextValidators.Size()) {
@@ -462,7 +462,7 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) {
{PubKey: vp, Power: 0},
}
assert.NotPanics(t, func() { state, err = blockExec.ApplyBlock(state, blockID, block) })
assert.NotPanics(t, func() { state, err = blockExec.ApplyBlock(ctx, state, blockID, block) })
assert.NotNil(t, err)
assert.NotEmpty(t, state.NextValidators.Validators)
}
+15 -5
View File
@@ -2,6 +2,7 @@ package state_test
import (
"bytes"
"context"
"fmt"
"time"
@@ -36,15 +37,17 @@ func newTestApp() proxy.AppConns {
}
func makeAndCommitGoodBlock(
ctx context.Context,
state sm.State,
height int64,
lastCommit *types.Commit,
proposerAddr []byte,
blockExec *sm.BlockExecutor,
privVals map[string]types.PrivValidator,
evidence []types.Evidence) (sm.State, types.BlockID, *types.Commit, error) {
evidence []types.Evidence,
) (sm.State, types.BlockID, *types.Commit, error) {
// A good block passes
state, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence)
state, blockID, err := makeAndApplyGoodBlock(ctx, state, height, lastCommit, proposerAddr, blockExec, evidence)
if err != nil {
return state, types.BlockID{}, nil, err
}
@@ -57,15 +60,22 @@ func makeAndCommitGoodBlock(
return state, blockID, commit, nil
}
func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commit, proposerAddr []byte,
blockExec *sm.BlockExecutor, evidence []types.Evidence) (sm.State, types.BlockID, error) {
func makeAndApplyGoodBlock(
ctx context.Context,
state sm.State,
height int64,
lastCommit *types.Commit,
proposerAddr []byte,
blockExec *sm.BlockExecutor,
evidence []types.Evidence,
) (sm.State, types.BlockID, error) {
block, _ := state.MakeBlock(height, factory.MakeTenTxs(height), lastCommit, evidence, proposerAddr)
if err := blockExec.ValidateBlock(state, block); err != nil {
return state, types.BlockID{}, err
}
blockID := types.BlockID{Hash: block.Hash(),
PartSetHeader: types.PartSetHeader{Total: 3, Hash: tmrand.Bytes(32)}}
state, err := blockExec.ApplyBlock(state, blockID, block)
state, err := blockExec.ApplyBlock(ctx, state, blockID, block)
if err != nil {
return state, types.BlockID{}, err
}
+10 -12
View File
@@ -14,6 +14,7 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/internal/state/indexer"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -91,10 +92,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
default:
}
conditions, err := q.Conditions()
if err != nil {
return nil, fmt.Errorf("failed to parse query conditions: %w", err)
}
conditions := q.Syntax()
// If there is an exact height query, return the result immediately
// (if it exists).
@@ -158,7 +156,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
continue
}
startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand))
startKey, err := orderedcode.Append(nil, c.Tag, c.Arg.Value())
if err != nil {
return nil, err
}
@@ -327,7 +325,7 @@ iter:
// matched.
func (idx *BlockerIndexer) match(
ctx context.Context,
c query.Condition,
c syntax.Condition,
startKeyBz []byte,
filteredHeights map[string][]byte,
firstRun bool,
@@ -342,7 +340,7 @@ func (idx *BlockerIndexer) match(
tmpHeights := make(map[string][]byte)
switch {
case c.Op == query.OpEqual:
case c.Op == syntax.TEq:
it, err := dbm.IteratePrefix(idx.store, startKeyBz)
if err != nil {
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
@@ -361,8 +359,8 @@ func (idx *BlockerIndexer) match(
return nil, err
}
case c.Op == query.OpExists:
prefix, err := orderedcode.Append(nil, c.CompositeKey)
case c.Op == syntax.TExists:
prefix, err := orderedcode.Append(nil, c.Tag)
if err != nil {
return nil, err
}
@@ -389,8 +387,8 @@ func (idx *BlockerIndexer) match(
return nil, err
}
case c.Op == query.OpContains:
prefix, err := orderedcode.Append(nil, c.CompositeKey)
case c.Op == syntax.TContains:
prefix, err := orderedcode.Append(nil, c.Tag)
if err != nil {
return nil, err
}
@@ -408,7 +406,7 @@ func (idx *BlockerIndexer) match(
continue
}
if strings.Contains(eventValue, c.Operand.(string)) {
if strings.Contains(eventValue, c.Arg.Value()) {
tmpHeights[string(it.Value())] = it.Value()
}
+9 -9
View File
@@ -94,39 +94,39 @@ func TestBlockIndexer(t *testing.T) {
results []int64
}{
"block.height = 100": {
q: query.MustParse("block.height = 100"),
q: query.MustCompile(`block.height = 100`),
results: []int64{},
},
"block.height = 5": {
q: query.MustParse("block.height = 5"),
q: query.MustCompile(`block.height = 5`),
results: []int64{5},
},
"begin_event.key1 = 'value1'": {
q: query.MustParse("begin_event.key1 = 'value1'"),
q: query.MustCompile(`begin_event.key1 = 'value1'`),
results: []int64{},
},
"begin_event.proposer = 'FCAA001'": {
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer = 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
"end_event.foo <= 5": {
q: query.MustParse("end_event.foo <= 5"),
q: query.MustCompile(`end_event.foo <= 5`),
results: []int64{2, 4},
},
"end_event.foo >= 100": {
q: query.MustParse("end_event.foo >= 100"),
q: query.MustCompile(`end_event.foo >= 100`),
results: []int64{1},
},
"block.height > 2 AND end_event.foo <= 8": {
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
q: query.MustCompile(`block.height > 2 AND end_event.foo <= 8`),
results: []int64{4, 6, 8},
},
"begin_event.proposer CONTAINS 'FFFFFFF'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FFFFFFF'`),
results: []int64{},
},
"begin_event.proposer CONTAINS 'FCAA001'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
}
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"strconv"
"github.com/google/orderedcode"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -85,10 +85,10 @@ func parseValueFromEventKey(key []byte) (string, error) {
return eventValue, nil
}
func lookForHeight(conditions []query.Condition) (int64, bool) {
func lookForHeight(conditions []syntax.Condition) (int64, bool) {
for _, c := range conditions {
if c.CompositeKey == types.BlockHeightKey && c.Op == query.OpEqual {
return c.Operand.(int64), true
if c.Tag == types.BlockHeightKey && c.Op == syntax.TEq {
return int64(c.Arg.Number()), true
}
}
@@ -75,7 +75,7 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
t.Cleanup(service.Wait)
// publish block with txs
err = eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
err = eventBus.PublishEventNewBlockHeader(ctx, types.EventDataNewBlockHeader{
Header: types.Header{Height: 1},
NumTxs: int64(2),
})
@@ -86,7 +86,7 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
Tx: types.Tx("foo"),
Result: abci.ResponseDeliverTx{Code: 0},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1})
err = eventBus.PublishEventTx(ctx, types.EventDataTx{TxResult: *txResult1})
require.NoError(t, err)
txResult2 := &abci.TxResult{
Height: 1,
@@ -94,7 +94,7 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
Tx: types.Tx("bar"),
Result: abci.ResponseDeliverTx{Code: 0},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2})
err = eventBus.PublishEventTx(ctx, types.EventDataTx{TxResult: *txResult2})
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
+29 -15
View File
@@ -3,7 +3,7 @@ package indexer
import (
"time"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
)
// QueryRanges defines a mapping between a composite event key and a QueryRange.
@@ -77,32 +77,32 @@ func (qr QueryRange) UpperBoundValue() interface{} {
// LookForRanges returns a mapping of QueryRanges and the matching indexes in
// the provided query conditions.
func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []int) {
func LookForRanges(conditions []syntax.Condition) (ranges QueryRanges, indexes []int) {
ranges = make(QueryRanges)
for i, c := range conditions {
if IsRangeOperation(c.Op) {
r, ok := ranges[c.CompositeKey]
r, ok := ranges[c.Tag]
if !ok {
r = QueryRange{Key: c.CompositeKey}
r = QueryRange{Key: c.Tag}
}
switch c.Op {
case query.OpGreater:
r.LowerBound = c.Operand
case syntax.TGt:
r.LowerBound = conditionArg(c)
case query.OpGreaterEqual:
case syntax.TGeq:
r.IncludeLowerBound = true
r.LowerBound = c.Operand
r.LowerBound = conditionArg(c)
case query.OpLess:
r.UpperBound = c.Operand
case syntax.TLt:
r.UpperBound = conditionArg(c)
case query.OpLessEqual:
case syntax.TLeq:
r.IncludeUpperBound = true
r.UpperBound = c.Operand
r.UpperBound = conditionArg(c)
}
ranges[c.CompositeKey] = r
ranges[c.Tag] = r
indexes = append(indexes, i)
}
}
@@ -112,12 +112,26 @@ func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []
// IsRangeOperation returns a boolean signifying if a query Operator is a range
// operation or not.
func IsRangeOperation(op query.Operator) bool {
func IsRangeOperation(op syntax.Token) bool {
switch op {
case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
case syntax.TGt, syntax.TGeq, syntax.TLt, syntax.TLeq:
return true
default:
return false
}
}
func conditionArg(c syntax.Condition) interface{} {
if c.Arg == nil {
return nil
}
switch c.Arg.Type {
case syntax.TNumber:
return int64(c.Arg.Number())
case syntax.TTime, syntax.TDate:
return c.Arg.Time()
default:
return c.Arg.Value() // string
}
}
+13 -13
View File
@@ -111,39 +111,39 @@ func TestBlockFuncs(t *testing.T) {
results []int64
}{
"block.height = 100": {
q: query.MustParse("block.height = 100"),
q: query.MustCompile(`block.height = 100`),
results: []int64{},
},
"block.height = 5": {
q: query.MustParse("block.height = 5"),
q: query.MustCompile(`block.height = 5`),
results: []int64{5},
},
"begin_event.key1 = 'value1'": {
q: query.MustParse("begin_event.key1 = 'value1'"),
q: query.MustCompile(`begin_event.key1 = 'value1'`),
results: []int64{},
},
"begin_event.proposer = 'FCAA001'": {
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer = 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
"end_event.foo <= 5": {
q: query.MustParse("end_event.foo <= 5"),
q: query.MustCompile(`end_event.foo <= 5`),
results: []int64{2, 4},
},
"end_event.foo >= 100": {
q: query.MustParse("end_event.foo >= 100"),
q: query.MustCompile(`end_event.foo >= 100`),
results: []int64{1},
},
"block.height > 2 AND end_event.foo <= 8": {
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
q: query.MustCompile(`block.height > 2 AND end_event.foo <= 8`),
results: []int64{4, 6, 8},
},
"begin_event.proposer CONTAINS 'FFFFFFF'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FFFFFFF'`),
results: []int64{},
},
"begin_event.proposer CONTAINS 'FCAA001'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
}
@@ -175,7 +175,7 @@ func TestTxSearchWithCancelation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number = 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number = 1`))
assert.NoError(t, err)
assert.Empty(t, results)
}
@@ -249,7 +249,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.SearchTxEvents(ctx, query.MustParse(tc.q))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(tc.q))
require.NoError(t, err)
for _, txr := range results {
for _, tr := range tc.results {
@@ -273,7 +273,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
ctx := context.Background()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
assert.Len(t, results, 1)
@@ -330,7 +330,7 @@ func TestTxSearchMultipleTxs(t *testing.T) {
ctx := context.Background()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
require.Len(t, results, 3)
+2
View File
@@ -39,6 +39,8 @@ func NewEventSink(connStr, chainID string) (*EventSink, error) {
db, err := sql.Open(driverName, connStr)
if err != nil {
return nil, err
} else if err := db.Ping(); err != nil {
return nil, err
}
return &EventSink{
-2
View File
@@ -13,8 +13,6 @@ import (
// 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
+17 -19
View File
@@ -14,6 +14,7 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
indexer "github.com/tendermint/tendermint/internal/state/indexer"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -148,10 +149,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul
filteredHashes := make(map[string][]byte)
// get a list of conditions (like "tx.height > 5")
conditions, err := q.Conditions()
if err != nil {
return nil, fmt.Errorf("error during parsing conditions from query: %w", err)
}
conditions := q.Syntax()
// if there is a hash condition, return the result immediately
hash, ok, err := lookForHash(conditions)
@@ -238,10 +236,10 @@ hashes:
return results, nil
}
func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) {
func lookForHash(conditions []syntax.Condition) (hash []byte, ok bool, err error) {
for _, c := range conditions {
if c.CompositeKey == types.TxHashKey {
decoded, err := hex.DecodeString(c.Operand.(string))
if c.Tag == types.TxHashKey {
decoded, err := hex.DecodeString(c.Arg.Value())
return decoded, true, err
}
}
@@ -249,10 +247,10 @@ func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error)
}
// lookForHeight returns a height if there is an "height=X" condition.
func lookForHeight(conditions []query.Condition) (height int64) {
func lookForHeight(conditions []syntax.Condition) (height int64) {
for _, c := range conditions {
if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual {
return c.Operand.(int64)
if c.Tag == types.TxHeightKey && c.Op == syntax.TEq {
return int64(c.Arg.Number())
}
}
return 0
@@ -265,7 +263,7 @@ func lookForHeight(conditions []query.Condition) (height int64) {
// NOTE: filteredHashes may be empty if no previous condition has matched.
func (txi *TxIndex) match(
ctx context.Context,
c query.Condition,
c syntax.Condition,
startKeyBz []byte,
filteredHashes map[string][]byte,
firstRun bool,
@@ -279,7 +277,7 @@ func (txi *TxIndex) match(
tmpHashes := make(map[string][]byte)
switch {
case c.Op == query.OpEqual:
case c.Op == syntax.TEq:
it, err := dbm.IteratePrefix(txi.store, startKeyBz)
if err != nil {
panic(err)
@@ -301,10 +299,10 @@ func (txi *TxIndex) match(
panic(err)
}
case c.Op == query.OpExists:
case c.Op == syntax.TExists:
// XXX: can't use startKeyBz here because c.Operand is nil
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.Tag))
if err != nil {
panic(err)
}
@@ -325,11 +323,11 @@ func (txi *TxIndex) match(
panic(err)
}
case c.Op == query.OpContains:
case c.Op == syntax.TContains:
// XXX: startKey does not apply here.
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.Tag))
if err != nil {
panic(err)
}
@@ -341,7 +339,7 @@ func (txi *TxIndex) match(
if err != nil {
continue
}
if strings.Contains(value, c.Operand.(string)) {
if strings.Contains(value, c.Arg.Value()) {
tmpHashes[string(it.Value())] = it.Value()
}
@@ -577,8 +575,8 @@ func prefixFromCompositeKeyAndValue(compositeKey, value string) []byte {
}
// a small utility function for getting a keys prefix based on a condition and a height
func prefixForCondition(c query.Condition, height int64) []byte {
key := prefixFromCompositeKeyAndValue(c.CompositeKey, fmt.Sprintf("%v", c.Operand))
func prefixForCondition(c syntax.Condition, height int64) []byte {
key := prefixFromCompositeKeyAndValue(c.Tag, c.Arg.Value())
if height > 0 {
var err error
key, err = orderedcode.Append(key, height)
@@ -60,7 +60,7 @@ func BenchmarkTxSearch(b *testing.B) {
}
}
txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50")
txQuery := query.MustCompile(`transfer.address = 'address_43' AND transfer.amount = 50`)
b.ResetTimer()
+5 -5
View File
@@ -131,7 +131,7 @@ func TestTxSearch(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.Search(ctx, query.MustParse(tc.q))
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
assert.NoError(t, err)
assert.Len(t, results, tc.resultsLength)
@@ -157,7 +157,7 @@ func TestTxSearchWithCancelation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
results, err := indexer.Search(ctx, query.MustParse("account.number = 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number = 1`))
assert.NoError(t, err)
assert.Empty(t, results)
}
@@ -230,7 +230,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.Search(ctx, query.MustParse(tc.q))
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
require.NoError(t, err)
for _, txr := range results {
for _, tr := range tc.results {
@@ -254,7 +254,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
ctx := context.Background()
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
assert.Len(t, results, 1)
@@ -311,7 +311,7 @@ func TestTxSearchMultipleTxs(t *testing.T) {
ctx := context.Background()
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
require.Len(t, results, 3)
+3 -1
View File
@@ -103,7 +103,7 @@ func TestValidateBlockHeader(t *testing.T) {
A good block passes
*/
var err error
state, _, lastCommit, err = makeAndCommitGoodBlock(
state, _, lastCommit, err = makeAndCommitGoodBlock(ctx,
state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil)
require.NoError(t, err, "height %d", height)
}
@@ -186,6 +186,7 @@ func TestValidateBlockCommit(t *testing.T) {
var err error
var blockID types.BlockID
state, blockID, lastCommit, err = makeAndCommitGoodBlock(
ctx,
state,
height,
lastCommit,
@@ -310,6 +311,7 @@ func TestValidateBlockEvidence(t *testing.T) {
var err error
state, _, lastCommit, err = makeAndCommitGoodBlock(
ctx,
state,
height,
lastCommit,
+1 -1
View File
@@ -18,7 +18,7 @@ func TestExample(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: "example-client",
Query: query.MustParse("abci.account.name='John'"),
Query: query.MustCompile(`abci.account.name='John'`),
}))
events := []abci.Event{
+7 -8
View File
@@ -106,7 +106,6 @@ type Server struct {
queue chan item
done <-chan struct{} // closed when server should exit
stop func() // signal the server to exit
pubs sync.RWMutex // excl: shutdown; shared: active publisher
exited chan struct{} // server exited
@@ -333,15 +332,15 @@ func (s *Server) PublishWithEvents(ctx context.Context, msg interface{}, events
return s.publish(ctx, msg, events)
}
// OnStop implements Service.OnStop by shutting down the server.
func (s *Server) OnStop() { s.stop() }
// OnStop implements part of the Service interface. It is a no-op.
func (s *Server) OnStop() {}
// Wait implements Service.Wait by blocking until the server has exited, then
// yielding to the base service wait.
func (s *Server) Wait() { <-s.exited; s.BaseService.Wait() }
// OnStart implements Service.OnStart by starting the server.
func (s *Server) OnStart(ctx context.Context) error { s.run(); return nil }
func (s *Server) OnStart(ctx context.Context) error { s.run(ctx); return nil }
// OnReset implements Service.OnReset. It has no effect for this service.
func (s *Server) OnReset() error { return nil }
@@ -363,11 +362,10 @@ func (s *Server) publish(ctx context.Context, data interface{}, events []types.E
}
}
func (s *Server) run() {
func (s *Server) run(ctx context.Context) {
// The server runs until ctx is canceled.
ctx, cancel := context.WithCancel(context.Background())
s.done = ctx.Done()
s.stop = cancel
queue := s.queue
// Shutdown monitor: When the context ends, wait for any active publish
// calls to exit, then close the queue to signal the sender to exit.
@@ -376,6 +374,7 @@ func (s *Server) run() {
s.pubs.Lock()
defer s.pubs.Unlock()
close(s.queue)
s.queue = nil
}()
s.exited = make(chan struct{})
@@ -383,7 +382,7 @@ func (s *Server) run() {
defer close(s.exited)
// Sender: Service the queue and forward messages to subscribers.
for it := range s.queue {
for it := range queue {
if err := s.send(it.Data, it.Events); err != nil {
s.Logger.Error("Error sending event", "err", err)
}
+22 -21
View File
@@ -27,7 +27,7 @@ func TestSubscribeWithArgs(t *testing.T) {
t.Run("DefaultLimit", func(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.Empty{},
Query: query.All,
}))
require.Equal(t, 1, s.NumClients())
@@ -39,7 +39,7 @@ func TestSubscribeWithArgs(t *testing.T) {
t.Run("PositiveLimit", func(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID + "-2",
Query: query.Empty{},
Query: query.All,
Limit: 10,
}))
require.NoError(t, s.Publish(ctx, "Aggamon"))
@@ -73,9 +73,9 @@ func TestObserverErrors(t *testing.T) {
s := newTestServer(ctx, t)
require.Error(t, s.Observe(ctx, nil, query.Empty{}))
require.Error(t, s.Observe(ctx, nil, query.All))
require.NoError(t, s.Observe(ctx, func(pubsub.Message) error { return nil }))
require.Error(t, s.Observe(ctx, func(pubsub.Message) error { return nil }, query.Empty{}))
require.Error(t, s.Observe(ctx, func(pubsub.Message) error { return nil }, query.All))
}
func TestPublishDoesNotBlock(t *testing.T) {
@@ -86,7 +86,7 @@ func TestPublishDoesNotBlock(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.Empty{},
Query: query.All,
}))
published := make(chan struct{})
go func() {
@@ -119,7 +119,7 @@ func TestSubscribeErrors(t *testing.T) {
t.Run("NegativeLimitErr", func(t *testing.T) {
_, err := s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.Empty{},
Query: query.All,
Limit: -5,
})
require.Error(t, err)
@@ -134,7 +134,7 @@ func TestSlowSubscriber(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.Empty{},
Query: query.All,
}))
require.NoError(t, s.Publish(ctx, "Fat Cobra"))
@@ -155,7 +155,7 @@ func TestDifferentClients(t *testing.T) {
sub1 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: "client-1",
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
events := []abci.Event{{
@@ -168,7 +168,7 @@ func TestDifferentClients(t *testing.T) {
sub2 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: "client-2",
Query: query.MustParse("tm.events.type='NewBlock' AND abci.account.name='Igor'"),
Query: query.MustCompile(`tm.events.type='NewBlock' AND abci.account.name='Igor'`),
}))
events = []abci.Event{
@@ -188,7 +188,8 @@ func TestDifferentClients(t *testing.T) {
sub3 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: "client-3",
Query: query.MustParse("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"),
Query: query.MustCompile(
`tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10`),
}))
events = []abci.Event{{
@@ -218,7 +219,7 @@ func TestSubscribeDuplicateKeys(t *testing.T) {
for i, tc := range testCases {
id := fmt.Sprintf("client-%d", i)
q := query.MustParse(tc.query)
q := query.MustCompile(tc.query)
t.Run(id, func(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: id,
@@ -261,7 +262,7 @@ func TestClientSubscribesTwice(t *testing.T) {
s := newTestServer(ctx, t)
q := query.MustParse("tm.events.type='NewBlock'")
q := query.MustCompile(`tm.events.type='NewBlock'`)
events := []abci.Event{{
Type: "tm.events",
Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}},
@@ -298,13 +299,13 @@ func TestUnsubscribe(t *testing.T) {
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
// Removing the subscription we just made should succeed.
require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
Subscriber: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
// Publishing should still work.
@@ -322,15 +323,15 @@ func TestClientUnsubscribesTwice(t *testing.T) {
newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
Subscriber: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
require.ErrorIs(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
Subscriber: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}), pubsub.ErrSubscriptionNotFound)
require.ErrorIs(t, s.UnsubscribeAll(ctx, clientID), pubsub.ErrSubscriptionNotFound)
}
@@ -343,13 +344,13 @@ func TestResubscribe(t *testing.T) {
args := pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.Empty{},
Query: query.All,
}
newTestSub(t).must(s.SubscribeWithArgs(ctx, args))
require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{
Subscriber: clientID,
Query: query.Empty{},
Query: query.All,
}))
sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, args))
@@ -366,11 +367,11 @@ func TestUnsubscribeAll(t *testing.T) {
sub1 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.MustParse("tm.events.type='NewBlock'"),
Query: query.MustCompile(`tm.events.type='NewBlock'`),
}))
sub2 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
ClientID: clientID,
Query: query.MustParse("tm.events.type='NewBlockHeader'"),
Query: query.MustCompile(`tm.events.type='NewBlockHeader'`),
}))
require.NoError(t, s.UnsubscribeAll(ctx, clientID))
-11
View File
@@ -1,11 +0,0 @@
gen_query_parser:
go get -u -v github.com/pointlander/peg
peg -inline -switch query.peg
fuzzy_test:
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz-build
go-fuzz-build github.com/tendermint/tendermint/libs/pubsub/query/fuzz_test
go-fuzz -bin=./fuzz_test-fuzz.zip -workdir=./fuzz_test/output
.PHONY: gen_query_parser fuzzy_test
+58
View File
@@ -0,0 +1,58 @@
package query_test
import (
"testing"
"github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/pubsub/query"
)
const testQuery = `tm.events.type='NewBlock' AND abci.account.name='Igor'`
var testEvents = []types.Event{
{
Type: "tm.events",
Attributes: []types.EventAttribute{{
Key: "index",
Value: "25",
}, {
Key: "type",
Value: "NewBlock",
}},
},
{
Type: "abci.account",
Attributes: []types.EventAttribute{{
Key: "name",
Value: "Anya",
}, {
Key: "name",
Value: "Igor",
}},
},
}
func BenchmarkParseCustom(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := query.New(testQuery)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkMatchCustom(b *testing.B) {
q, err := query.New(testQuery)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ok, err := q.Matches(testEvents)
if err != nil {
b.Fatal(err)
} else if !ok {
b.Error("no match")
}
}
}
-18
View File
@@ -1,18 +0,0 @@
package query
import (
"github.com/tendermint/tendermint/abci/types"
)
// Empty query matches any set of events.
type Empty struct {
}
// Matches always returns true.
func (Empty) Matches(events []types.Event) (bool, error) {
return true, nil
}
func (Empty) String() string {
return "empty"
}
-55
View File
@@ -1,55 +0,0 @@
package query_test
import (
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/pubsub/query"
)
func TestEmptyQueryMatchesAnything(t *testing.T) {
q := query.Empty{}
testCases := []struct {
events []abci.Event
}{
{
[]abci.Event{},
},
{
[]abci.Event{
{
Type: "Asher",
Attributes: []abci.EventAttribute{{Key: "Roth"}},
},
},
},
{
[]abci.Event{
{
Type: "Route",
Attributes: []abci.EventAttribute{{Key: "66"}},
},
},
},
{
[]abci.Event{
{
Type: "Route",
Attributes: []abci.EventAttribute{{Key: "66"}},
},
{
Type: "Billy",
Attributes: []abci.EventAttribute{{Key: "Blue"}},
},
},
},
}
for _, tc := range testCases {
match, err := q.Matches(tc.events)
require.Nil(t, err)
require.True(t, match)
}
}
-30
View File
@@ -1,30 +0,0 @@
package fuzz_test
import (
"fmt"
"github.com/tendermint/tendermint/libs/pubsub/query"
)
func Fuzz(data []byte) int {
sdata := string(data)
q0, err := query.New(sdata)
if err != nil {
return 0
}
sdata1 := q0.String()
q1, err := query.New(sdata1)
if err != nil {
panic(err)
}
sdata2 := q1.String()
if sdata1 != sdata2 {
fmt.Printf("q0: %q\n", sdata1)
fmt.Printf("q1: %q\n", sdata2)
panic("query changed")
}
return 1
}
-97
View File
@@ -1,97 +0,0 @@
package query_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tendermint/tendermint/libs/pubsub/query"
)
// TODO: fuzzy testing?
func TestParser(t *testing.T) {
cases := []struct {
query string
valid bool
}{
{"tm.events.type='NewBlock'", true},
{"tm.events.type = 'NewBlock'", true},
{"tm.events.name = ''", true},
{"tm.events.type='TIME'", true},
{"tm.events.type='DATE'", true},
{"tm.events.type='='", true},
{"tm.events.type='TIME", false},
{"tm.events.type=TIME'", false},
{"tm.events.type==", false},
{"tm.events.type=NewBlock", false},
{">==", false},
{"tm.events.type 'NewBlock' =", false},
{"tm.events.type>'NewBlock'", false},
{"", false},
{"=", false},
{"='NewBlock'", false},
{"tm.events.type=", false},
{"tm.events.typeNewBlock", false},
{"tm.events.type'NewBlock'", false},
{"'NewBlock'", false},
{"NewBlock", false},
{"", false},
{"tm.events.type='NewBlock' AND abci.account.name='Igor'", true},
{"tm.events.type='NewBlock' AND", false},
{"tm.events.type='NewBlock' AN", false},
{"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false},
{"AND tm.events.type='NewBlock' ", false},
{"abci.account.name CONTAINS 'Igor'", true},
{"tx.date > DATE 2013-05-03", true},
{"tx.date < DATE 2013-05-03", true},
{"tx.date <= DATE 2013-05-03", true},
{"tx.date >= DATE 2013-05-03", true},
{"tx.date >= DAT 2013-05-03", false},
{"tx.date <= DATE2013-05-03", false},
{"tx.date <= DATE -05-03", false},
{"tx.date >= DATE 20130503", false},
{"tx.date >= DATE 2013+01-03", false},
// incorrect year, month, day
{"tx.date >= DATE 0013-01-03", false},
{"tx.date >= DATE 2013-31-03", false},
{"tx.date >= DATE 2013-01-83", false},
{"tx.date > TIME 2013-05-03T14:45:00+07:00", true},
{"tx.date < TIME 2013-05-03T14:45:00-02:00", true},
{"tx.date <= TIME 2013-05-03T14:45:00Z", true},
{"tx.date >= TIME 2013-05-03T14:45:00Z", true},
{"tx.date >= TIME2013-05-03T14:45:00Z", false},
{"tx.date = IME 2013-05-03T14:45:00Z", false},
{"tx.date = TIME 2013-05-:45:00Z", false},
{"tx.date >= TIME 2013-05-03T14:45:00", false},
{"tx.date >= TIME 0013-00-00T14:45:00Z", false},
{"tx.date >= TIME 2013+05=03T14:45:00Z", false},
{"account.balance=100", true},
{"account.balance >= 200", true},
{"account.balance >= -300", false},
{"account.balance >>= 400", false},
{"account.balance=33.22.1", false},
{"slashing.amount EXISTS", true},
{"slashing.amount EXISTS AND account.balance=100", true},
{"account.balance=100 AND slashing.amount EXISTS", true},
{"slashing EXISTS", true},
{"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true},
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false},
}
for _, c := range cases {
_, err := query.New(c.query)
if c.valid {
assert.NoErrorf(t, err, "Query was '%s'", c.query)
} else {
assert.Errorf(t, err, "Query was '%s'", c.query)
}
}
}
-3
View File
@@ -1,3 +0,0 @@
package query
//go:generate peg -inline -switch query.peg
+280 -480
View File
@@ -1,527 +1,327 @@
// Package query provides a parser for a custom query format:
// Package query implements the custom query format used to filter event
// subscriptions in Tendermint.
//
// abci.invoice.number=22 AND abci.invoice.owner=Ivan
// Query expressions describe properties of events and their attributes, using
// strings like:
//
// See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar.
// More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics
// abci.invoice.number = 22 AND abci.invoice.owner = 'Ivan'
//
// Query expressions can handle attribute values encoding numbers, strings,
// dates, and timestamps. The complete query grammar is described in the
// query/syntax package.
//
// It has a support for numbers (integer and floating point), dates and times.
package query
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
)
var (
numRegex = regexp.MustCompile(`([0-9\.]+)`)
)
// All is a query that matches all events.
var All *Query
// Query holds the query string and the query parser.
// A Query is the compiled form of a query.
type Query struct {
str string
parser *QueryParser
ast syntax.Query
conds []condition
}
// Condition represents a single condition within a query and consists of composite key
// (e.g. "tx.gas"), operator (e.g. "=") and operand (e.g. "7").
type Condition struct {
CompositeKey string
Op Operator
Operand interface{}
}
// New parses the given string and returns a query or error if the string is
// invalid.
func New(s string) (*Query, error) {
p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)}
p.Init()
if err := p.Parse(); err != nil {
// New parses and compiles the query expression into an executable query.
func New(query string) (*Query, error) {
ast, err := syntax.Parse(query)
if err != nil {
return nil, err
}
return &Query{str: s, parser: p}, nil
return Compile(ast)
}
// MustParse turns the given string into a query or panics; for tests or others
// cases where you know the string is valid.
func MustParse(s string) *Query {
q, err := New(s)
// MustCompile compiles the query expression into an executable query.
// In case of error, MustCompile will panic.
//
// This is intended for use in program initialization; use query.New if you
// need to check errors.
func MustCompile(query string) *Query {
q, err := New(query)
if err != nil {
panic(fmt.Sprintf("failed to parse %s: %v", s, err))
panic(err)
}
return q
}
// String returns the original string.
// Compile compiles the given query AST so it can be used to match events.
func Compile(ast syntax.Query) (*Query, error) {
conds := make([]condition, len(ast))
for i, q := range ast {
cond, err := compileCondition(q)
if err != nil {
return nil, fmt.Errorf("compile %s: %w", q, err)
}
conds[i] = cond
}
return &Query{ast: ast, conds: conds}, nil
}
// Matches satisfies part of the pubsub.Query interface. This implementation
// never reports an error. A nil *Query matches all events.
func (q *Query) Matches(events []types.Event) (bool, error) {
if q == nil {
return true, nil
}
return q.matchesEvents(events), nil
}
// String matches part of the pubsub.Query interface.
func (q *Query) String() string {
return q.str
if q == nil {
return "<empty>"
}
return q.ast.String()
}
// Operator is an operator that defines some kind of relation between composite key and
// operand (equality, etc.).
type Operator uint8
const (
// "<="
OpLessEqual Operator = iota
// ">="
OpGreaterEqual
// "<"
OpLess
// ">"
OpGreater
// "="
OpEqual
// "CONTAINS"; used to check if a string contains a certain sub string.
OpContains
// "EXISTS"; used to check if a certain event attribute is present.
OpExists
)
const (
// DateLayout defines a layout for all dates (`DATE date`)
DateLayout = "2006-01-02"
// TimeLayout defines a layout for all times (`TIME time`)
TimeLayout = time.RFC3339
)
// Conditions returns a list of conditions. It returns an error if there is any
// error with the provided grammar in the Query.
func (q *Query) Conditions() ([]Condition, error) {
var (
eventAttr string
op Operator
)
conditions := make([]Condition, 0)
buffer, begin, end := q.parser.Buffer, 0, 0
// tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7")
for token := range q.parser.Tokens() {
switch token.pegRule {
case rulePegText:
begin, end = int(token.begin), int(token.end)
case ruletag:
eventAttr = buffer[begin:end]
case rulele:
op = OpLessEqual
case rulege:
op = OpGreaterEqual
case rulel:
op = OpLess
case ruleg:
op = OpGreater
case ruleequal:
op = OpEqual
case rulecontains:
op = OpContains
case ruleexists:
op = OpExists
conditions = append(conditions, Condition{eventAttr, op, nil})
case rulevalue:
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
valueWithoutSingleQuotes := buffer[begin+1 : end-1]
conditions = append(conditions, Condition{eventAttr, op, valueWithoutSingleQuotes})
case rulenumber:
number := buffer[begin:end]
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number
value, err := strconv.ParseFloat(number, 64)
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)",
err, number,
)
return nil, err
}
conditions = append(conditions, Condition{eventAttr, op, value})
} else {
value, err := strconv.ParseInt(number, 10, 64)
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)",
err, number,
)
return nil, err
}
conditions = append(conditions, Condition{eventAttr, op, value})
}
case ruletime:
value, err := time.Parse(TimeLayout, buffer[begin:end])
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)",
err, buffer[begin:end],
)
return nil, err
}
conditions = append(conditions, Condition{eventAttr, op, value})
case ruledate:
value, err := time.Parse("2006-01-02", buffer[begin:end])
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)",
err, buffer[begin:end],
)
return nil, err
}
conditions = append(conditions, Condition{eventAttr, op, value})
}
// Syntax returns the syntax tree representation of q.
func (q *Query) Syntax() syntax.Query {
if q == nil {
return nil
}
return conditions, nil
return q.ast
}
// Matches returns true if the query matches against any event in the given set
// of events, false otherwise. For each event, a match exists if the query is
// matched against *any* value in a slice of values. An error is returned if
// any attempted event match returns an error.
//
// For example, query "name=John" matches events = {"name": ["John", "Eric"]}.
// More examples could be found in parser_test.go and query_test.go.
func (q *Query) Matches(rawEvents []types.Event) (bool, error) {
if len(rawEvents) == 0 {
return false, nil
}
events := flattenEvents(rawEvents)
var (
eventAttr string
op Operator
)
buffer, begin, end := q.parser.Buffer, 0, 0
// tokens must be in the following order:
// tag ("tx.gas") -> operator ("=") -> operand ("7")
for token := range q.parser.Tokens() {
switch token.pegRule {
case rulePegText:
begin, end = int(token.begin), int(token.end)
case ruletag:
eventAttr = buffer[begin:end]
case rulele:
op = OpLessEqual
case rulege:
op = OpGreaterEqual
case rulel:
op = OpLess
case ruleg:
op = OpGreater
case ruleequal:
op = OpEqual
case rulecontains:
op = OpContains
case ruleexists:
op = OpExists
if strings.Contains(eventAttr, ".") {
// Searching for a full "type.attribute" event.
_, ok := events[eventAttr]
if !ok {
return false, nil
}
} else {
foundEvent := false
loop:
for compositeKey := range events {
if strings.Index(compositeKey, eventAttr) == 0 {
foundEvent = true
break loop
}
}
if !foundEvent {
return false, nil
}
}
case rulevalue:
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
valueWithoutSingleQuotes := buffer[begin+1 : end-1]
// see if the triplet (event attribute, operator, operand) matches any event
// "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" }
match, err := match(eventAttr, op, reflect.ValueOf(valueWithoutSingleQuotes), events)
if err != nil {
return false, err
}
if !match {
return false, nil
}
case rulenumber:
number := buffer[begin:end]
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number
value, err := strconv.ParseFloat(number, 64)
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)",
err, number,
)
return false, err
}
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
if err != nil {
return false, err
}
if !match {
return false, nil
}
} else {
value, err := strconv.ParseInt(number, 10, 64)
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)",
err, number,
)
return false, err
}
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
if err != nil {
return false, err
}
if !match {
return false, nil
}
}
case ruletime:
value, err := time.Parse(TimeLayout, buffer[begin:end])
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)",
err, buffer[begin:end],
)
return false, err
}
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
if err != nil {
return false, err
}
if !match {
return false, nil
}
case ruledate:
value, err := time.Parse("2006-01-02", buffer[begin:end])
if err != nil {
err = fmt.Errorf(
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)",
err, buffer[begin:end],
)
return false, err
}
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
if err != nil {
return false, err
}
if !match {
return false, nil
}
// matchesEvents reports whether all the conditions match the given events.
func (q *Query) matchesEvents(events []types.Event) bool {
for _, cond := range q.conds {
if !cond.matchesAny(events) {
return false
}
}
return true, nil
return len(events) != 0
}
// match returns true if the given triplet (attribute, operator, operand) matches
// any value in an event for that attribute. If any match fails with an error,
// that error is returned.
//
// First, it looks up the key in the events and if it finds one, tries to compare
// all the values from it to the operand using the operator.
//
// "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]}
func match(attr string, op Operator, operand reflect.Value, events map[string][]string) (bool, error) {
// look up the tag from the query in tags
values, ok := events[attr]
if !ok {
return false, nil
}
for _, value := range values {
// return true if any value in the set of the event's values matches
match, err := matchValue(value, op, operand)
if err != nil {
return false, err
}
if match {
return true, nil
}
}
return false, nil
// A condition is a compiled match condition. A condition matches an event if
// the event has the designated type, contains an attribute with the given
// name, and the match function returns true for the attribute value.
type condition struct {
tag string // e.g., "tx.hash"
match func(s string) bool
}
// matchValue will attempt to match a string value against an operator an
// operand. A boolean is returned representing the match result. It will return
// an error if the value cannot be parsed and matched against the operand type.
func matchValue(value string, op Operator, operand reflect.Value) (bool, error) {
switch operand.Kind() {
case reflect.Struct: // time
operandAsTime := operand.Interface().(time.Time)
// try our best to convert value from events to time.Time
var (
v time.Time
err error
)
if strings.ContainsAny(value, "T") {
v, err = time.Parse(TimeLayout, value)
} else {
v, err = time.Parse(DateLayout, value)
}
if err != nil {
return false, fmt.Errorf("failed to convert value %v from event attribute to time.Time: %w", value, err)
}
switch op {
case OpLessEqual:
return (v.Before(operandAsTime) || v.Equal(operandAsTime)), nil
case OpGreaterEqual:
return (v.Equal(operandAsTime) || v.After(operandAsTime)), nil
case OpLess:
return v.Before(operandAsTime), nil
case OpGreater:
return v.After(operandAsTime), nil
case OpEqual:
return v.Equal(operandAsTime), nil
}
case reflect.Float64:
var v float64
operandFloat64 := operand.Interface().(float64)
filteredValue := numRegex.FindString(value)
// try our best to convert value from tags to float64
v, err := strconv.ParseFloat(filteredValue, 64)
if err != nil {
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err)
}
switch op {
case OpLessEqual:
return v <= operandFloat64, nil
case OpGreaterEqual:
return v >= operandFloat64, nil
case OpLess:
return v < operandFloat64, nil
case OpGreater:
return v > operandFloat64, nil
case OpEqual:
return v == operandFloat64, nil
}
case reflect.Int64:
var v int64
operandInt := operand.Interface().(int64)
filteredValue := numRegex.FindString(value)
// if value looks like float, we try to parse it as float
if strings.ContainsAny(filteredValue, ".") {
v1, err := strconv.ParseFloat(filteredValue, 64)
if err != nil {
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err)
}
v = int64(v1)
} else {
var err error
// try our best to convert value from tags to int64
v, err = strconv.ParseInt(filteredValue, 10, 64)
if err != nil {
return false, fmt.Errorf("failed to convert value %v from event attribute to int64: %w", filteredValue, err)
}
}
switch op {
case OpLessEqual:
return v <= operandInt, nil
case OpGreaterEqual:
return v >= operandInt, nil
case OpLess:
return v < operandInt, nil
case OpGreater:
return v > operandInt, nil
case OpEqual:
return v == operandInt, nil
}
case reflect.String:
switch op {
case OpEqual:
return value == operand.String(), nil
case OpContains:
return strings.Contains(value, operand.String()), nil
}
default:
return false, fmt.Errorf("unknown kind of operand %v", operand.Kind())
// findAttr returns a slice of attribute values from event matching the
// condition tag, and reports whether the event type strictly equals the
// condition tag.
func (c condition) findAttr(event types.Event) ([]string, bool) {
if !strings.HasPrefix(c.tag, event.Type) {
return nil, false // type does not match tag
} else if len(c.tag) == len(event.Type) {
return nil, true // type == tag
}
return false, nil
var vals []string
for _, attr := range event.Attributes {
fullName := event.Type + "." + attr.Key
if fullName == c.tag {
vals = append(vals, attr.Value)
}
}
return vals, false
}
func flattenEvents(events []types.Event) map[string][]string {
flattened := make(map[string][]string)
// matchesAny reports whether c matches at least one of the given events.
func (c condition) matchesAny(events []types.Event) bool {
for _, event := range events {
if len(event.Type) == 0 {
continue
}
for _, attr := range event.Attributes {
if len(attr.Key) == 0 {
continue
}
compositeEvent := fmt.Sprintf("%s.%s", event.Type, attr.Key)
flattened[compositeEvent] = append(flattened[compositeEvent], attr.Value)
if c.matchesEvent(event) {
return true
}
}
return flattened
return false
}
// matchesEvent reports whether c matches the given event.
func (c condition) matchesEvent(event types.Event) bool {
vs, tagEqualsType := c.findAttr(event)
if len(vs) == 0 {
// As a special case, a condition tag that exactly matches the event type
// is matched against an empty string. This allows existence checks to
// work for type-only queries.
if tagEqualsType {
return c.match("")
}
return false
}
// At this point, we have candidate values.
for _, v := range vs {
if c.match(v) {
return true
}
}
return false
}
func compileCondition(cond syntax.Condition) (condition, error) {
out := condition{tag: cond.Tag}
// Handle existence checks separately to simplify the logic below for
// comparisons that take arguments.
if cond.Op == syntax.TExists {
out.match = func(string) bool { return true }
return out, nil
}
// All the other operators require an argument.
if cond.Arg == nil {
return condition{}, fmt.Errorf("missing argument for %v", cond.Op)
}
// Precompile the argument value matcher.
argType := cond.Arg.Type
var argValue interface{}
switch argType {
case syntax.TString:
argValue = cond.Arg.Value()
case syntax.TNumber:
argValue = cond.Arg.Number()
case syntax.TTime, syntax.TDate:
argValue = cond.Arg.Time()
default:
return condition{}, fmt.Errorf("unknown argument type %v", argType)
}
mcons := opTypeMap[cond.Op][argType]
if mcons == nil {
return condition{}, fmt.Errorf("invalid op/arg combination (%v, %v)", cond.Op, argType)
}
out.match = mcons(argValue)
return out, nil
}
// TODO(creachadair): The existing implementation allows anything number shaped
// to be treated as a number. This preserves the parts of that behavior we had
// tests for, but we should probably get rid of that.
var extractNum = regexp.MustCompile(`^\d+(\.\d+)?`)
func parseNumber(s string) (float64, error) {
return strconv.ParseFloat(extractNum.FindString(s), 64)
}
// A map of operator ⇒ argtype ⇒ match-constructor.
// An entry does not exist if the combination is not valid.
//
// Disable the dupl lint for this map. The result isn't even correct.
//nolint:dupl
var opTypeMap = map[syntax.Token]map[syntax.Token]func(interface{}) func(string) bool{
syntax.TContains: {
syntax.TString: func(v interface{}) func(string) bool {
return func(s string) bool {
return strings.Contains(s, v.(string))
}
},
},
syntax.TEq: {
syntax.TString: func(v interface{}) func(string) bool {
return func(s string) bool { return s == v.(string) }
},
syntax.TNumber: func(v interface{}) func(string) bool {
return func(s string) bool {
w, err := parseNumber(s)
return err == nil && w == v.(float64)
}
},
syntax.TDate: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseDate(s)
return err == nil && ts.Equal(v.(time.Time))
}
},
syntax.TTime: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseTime(s)
return err == nil && ts.Equal(v.(time.Time))
}
},
},
syntax.TLt: {
syntax.TNumber: func(v interface{}) func(string) bool {
return func(s string) bool {
w, err := parseNumber(s)
return err == nil && w < v.(float64)
}
},
syntax.TDate: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseDate(s)
return err == nil && ts.Before(v.(time.Time))
}
},
syntax.TTime: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseTime(s)
return err == nil && ts.Before(v.(time.Time))
}
},
},
syntax.TLeq: {
syntax.TNumber: func(v interface{}) func(string) bool {
return func(s string) bool {
w, err := parseNumber(s)
return err == nil && w <= v.(float64)
}
},
syntax.TDate: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseDate(s)
return err == nil && !ts.After(v.(time.Time))
}
},
syntax.TTime: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseTime(s)
return err == nil && !ts.After(v.(time.Time))
}
},
},
syntax.TGt: {
syntax.TNumber: func(v interface{}) func(string) bool {
return func(s string) bool {
w, err := parseNumber(s)
return err == nil && w > v.(float64)
}
},
syntax.TDate: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseDate(s)
return err == nil && ts.After(v.(time.Time))
}
},
syntax.TTime: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseTime(s)
return err == nil && ts.After(v.(time.Time))
}
},
},
syntax.TGeq: {
syntax.TNumber: func(v interface{}) func(string) bool {
return func(s string) bool {
w, err := parseNumber(s)
return err == nil && w >= v.(float64)
}
},
syntax.TDate: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseDate(s)
return err == nil && !ts.Before(v.(time.Time))
}
},
syntax.TTime: func(v interface{}) func(string) bool {
return func(s string) bool {
ts, err := syntax.ParseTime(s)
return err == nil && !ts.Before(v.(time.Time))
}
},
},
}
-35
View File
@@ -1,35 +0,0 @@
package query
type QueryParser Peg {
}
e <- '\"' condition ( ' '+ and ' '+ condition )* '\"' !.
condition <- tag ' '* (le ' '* (number / time / date)
/ ge ' '* (number / time / date)
/ l ' '* (number / time / date)
/ g ' '* (number / time / date)
/ equal ' '* (number / time / date / value)
/ contains ' '* value
/ exists
)
tag <- < (![ \t\n\r\\()"'=><] .)+ >
value <- < '\'' (!["'] .)* '\''>
number <- < ('0'
/ [1-9] digit* ('.' digit*)?) >
digit <- [0-9]
time <- "TIME " < year '-' month '-' day 'T' digit digit ':' digit digit ':' digit digit (('-' / '+') digit digit ':' digit digit / 'Z') >
date <- "DATE " < year '-' month '-' day >
year <- ('1' / '2') digit digit digit
month <- ('0' / '1') digit
day <- ('0' / '1' / '2' / '3') digit
and <- "AND"
equal <- "="
contains <- "CONTAINS"
exists <- "EXISTS"
le <- "<="
ge <- ">="
l <- "<"
g <- ">"
File diff suppressed because it is too large Load Diff
+242 -214
View File
@@ -6,242 +6,270 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/pubsub"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
)
func expandEvents(flattenedEvents map[string][]string) []abci.Event {
events := make([]abci.Event, len(flattenedEvents))
var _ pubsub.Query = (*query.Query)(nil)
for composite, values := range flattenedEvents {
tokens := strings.Split(composite, ".")
attrs := make([]abci.EventAttribute, len(values))
for i, v := range values {
attrs[i] = abci.EventAttribute{
Key: tokens[len(tokens)-1],
Value: v,
}
}
events = append(events, abci.Event{
Type: strings.Join(tokens[:len(tokens)-1], "."),
Attributes: attrs,
})
}
return events
// Example events from the OpenAPI documentation:
// https://github.com/tendermint/tendermint/blob/master/rpc/openapi/openapi.yaml
//
// Redactions:
//
// - Add an explicit "tm" event for the built-in attributes.
// - Remove Index fields (not relevant to tests).
// - Add explicit balance values (to use in tests).
//
var apiEvents = []types.Event{
{
Type: "tm",
Attributes: []types.EventAttribute{
{Key: "event", Value: "Tx"},
{Key: "hash", Value: "XYZ"},
{Key: "height", Value: "5"},
},
},
{
Type: "rewards.withdraw",
Attributes: []types.EventAttribute{
{Key: "address", Value: "AddrA"},
{Key: "source", Value: "SrcX"},
{Key: "amount", Value: "100"},
{Key: "balance", Value: "1500"},
},
},
{
Type: "rewards.withdraw",
Attributes: []types.EventAttribute{
{Key: "address", Value: "AddrB"},
{Key: "source", Value: "SrcY"},
{Key: "amount", Value: "45"},
{Key: "balance", Value: "999"},
},
},
{
Type: "transfer",
Attributes: []types.EventAttribute{
{Key: "sender", Value: "AddrC"},
{Key: "recipient", Value: "AddrD"},
{Key: "amount", Value: "160"},
},
},
}
func TestMatches(t *testing.T) {
func TestCompiledMatches(t *testing.T) {
var (
txDate = "2017-01-01"
txTime = "2018-05-03T14:45:00Z"
)
testCases := []struct {
s string
events map[string][]string
err bool
matches bool
matchErr bool
s string
events []types.Event
matches bool
}{
{"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}}, false, true, false},
{"tx.gas > 7", map[string][]string{"tx.gas": {"8"}}, false, true, false},
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8stake"}}, false, true, false},
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8.045stake"}}, false, true, false},
{"transfer.amount > 7.043", map[string][]string{"transfer.amount": {"8.045stake"}}, false, true, false},
{"transfer.amount > 8.045", map[string][]string{"transfer.amount": {"8.045stake"}}, false, false, false},
{"tx.gas > 7 AND tx.gas < 9", map[string][]string{"tx.gas": {"8"}}, false, true, false},
{"body.weight >= 3.5", map[string][]string{"body.weight": {"3.5"}}, false, true, false},
{"account.balance < 1000.0", map[string][]string{"account.balance": {"900"}}, false, true, false},
{"apples.kg <= 4", map[string][]string{"apples.kg": {"4.0"}}, false, true, false},
{"body.weight >= 4.5", map[string][]string{"body.weight": {fmt.Sprintf("%v", float32(4.5))}}, false, true, false},
{
"oranges.kg < 4 AND watermellons.kg > 10",
map[string][]string{"oranges.kg": {"3"}, "watermellons.kg": {"12"}},
false,
true,
false,
},
{"peaches.kg < 4", map[string][]string{"peaches.kg": {"5"}}, false, false, false},
{
"tx.date > DATE 2017-01-01",
map[string][]string{"tx.date": {time.Now().Format(query.DateLayout)}},
false,
true,
false,
},
{"tx.date = DATE 2017-01-01", map[string][]string{"tx.date": {txDate}}, false, true, false},
{"tx.date = DATE 2018-01-01", map[string][]string{"tx.date": {txDate}}, false, false, false},
{
"tx.time >= TIME 2013-05-03T14:45:00Z",
map[string][]string{"tx.time": {time.Now().Format(query.TimeLayout)}},
false,
true,
false,
},
{"tx.time = TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {txTime}}, false, false, false},
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Igor,Ivan"}}, false, true, false},
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Pavel,Ivan"}}, false, false, false},
{"abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, false, true, false},
{
"abci.owner.name = 'Ivan'",
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
false,
true,
false,
},
{
"abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'",
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
false,
true,
false,
},
{
"abci.owner.name = 'Ivan' AND abci.owner.name = 'John'",
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
false,
false,
false,
},
{
"tm.events.type='NewBlock'",
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
false,
true,
false,
},
{
"app.name = 'fuzzed'",
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
false,
true,
false,
},
{
"tm.events.type='NewBlock' AND app.name = 'fuzzed'",
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
false,
true,
false,
},
{
"tm.events.type='NewHeader' AND app.name = 'fuzzed'",
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
false,
false,
false,
},
{"slash EXISTS",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"sl EXISTS",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"slash EXISTS",
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
false,
false,
false,
},
{"slash.reason EXISTS AND slash.power > 1000",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"slash.reason EXISTS AND slash.power > 1000",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"500"}},
false,
false,
false,
},
{"slash.reason EXISTS",
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
false,
false,
false,
},
{`tm.events.type='NewBlock'`,
newTestEvents(`tm|events.type=NewBlock`),
true},
{`tx.gas > 7`,
newTestEvents(`tx|gas=8`),
true},
{`transfer.amount > 7`,
newTestEvents(`transfer|amount=8stake`),
true},
{`transfer.amount > 7`,
newTestEvents(`transfer|amount=8.045`),
true},
{`transfer.amount > 7.043`,
newTestEvents(`transfer|amount=8.045stake`),
true},
{`transfer.amount > 8.045`,
newTestEvents(`transfer|amount=8.045stake`),
false},
{`tx.gas > 7 AND tx.gas < 9`,
newTestEvents(`tx|gas=8`),
true},
{`body.weight >= 3.5`,
newTestEvents(`body|weight=3.5`),
true},
{`account.balance < 1000.0`,
newTestEvents(`account|balance=900`),
true},
{`apples.kg <= 4`,
newTestEvents(`apples|kg=4.0`),
true},
{`body.weight >= 4.5`,
newTestEvents(`body|weight=4.5`),
true},
{`oranges.kg < 4 AND watermellons.kg > 10`,
newTestEvents(`oranges|kg=3`, `watermellons|kg=12`),
true},
{`peaches.kg < 4`,
newTestEvents(`peaches|kg=5`),
false},
{`tx.date > DATE 2017-01-01`,
newTestEvents(`tx|date=` + time.Now().Format(syntax.DateFormat)),
true},
{`tx.date = DATE 2017-01-01`,
newTestEvents(`tx|date=` + txDate),
true},
{`tx.date = DATE 2018-01-01`,
newTestEvents(`tx|date=` + txDate),
false},
{`tx.time >= TIME 2013-05-03T14:45:00Z`,
newTestEvents(`tx|time=` + time.Now().Format(syntax.TimeFormat)),
true},
{`tx.time = TIME 2013-05-03T14:45:00Z`,
newTestEvents(`tx|time=` + txTime),
false},
{`abci.owner.name CONTAINS 'Igor'`,
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
true},
{`abci.owner.name CONTAINS 'Igor'`,
newTestEvents(`abci|owner.name=Pavel|owner.name=Ivan`),
false},
{`abci.owner.name = 'Igor'`,
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
true},
{`abci.owner.name = 'Ivan'`,
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
true},
{`abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'`,
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
true},
{`abci.owner.name = 'Ivan' AND abci.owner.name = 'John'`,
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
false},
{`tm.events.type='NewBlock'`,
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
true},
{`app.name = 'fuzzed'`,
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
true},
{`tm.events.type='NewBlock' AND app.name = 'fuzzed'`,
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
true},
{`tm.events.type='NewHeader' AND app.name = 'fuzzed'`,
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
false},
{`slash EXISTS`,
newTestEvents(`slash|reason=missing_signature|power=6000`),
true},
{`slash EXISTS`,
newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
false},
{`slash.reason EXISTS AND slash.power > 1000`,
newTestEvents(`slash|reason=missing_signature|power=6000`),
true},
{`slash.reason EXISTS AND slash.power > 1000`,
newTestEvents(`slash|reason=missing_signature|power=500`),
false},
{`slash.reason EXISTS`,
newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
false},
// Test cases based on the OpenAPI examples.
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'`,
apiEvents, true},
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'SrcY'`,
apiEvents, true},
{`tm.event = 'Tx' AND transfer.sender = 'AddrA'`,
apiEvents, false},
{`tm.event = 'Tx' AND transfer.sender = 'AddrC'`,
apiEvents, true},
{`tm.event = 'Tx' AND transfer.sender = 'AddrZ'`,
apiEvents, false},
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'`,
apiEvents, false},
{`tm.event = 'Tx' AND rewards.withdraw.source = 'W'`,
apiEvents, false},
}
for _, tc := range testCases {
q, err := query.New(tc.s)
if !tc.err {
require.Nil(t, err)
}
require.NotNil(t, q, "Query '%s' should not be nil", tc.s)
// NOTE: The original implementation allowed arbitrary prefix matches on
// attribute tags, e.g., "sl" would match "slash".
//
// That is weird and probably wrong: "foo.ba" should not match "foo.bar",
// or there is no way to distinguish the case where there were two values
// for "foo.bar" or one value each for "foo.ba" and "foo.bar".
//
// Apart from a single test case, I could not find any attested usage of
// this implementation detail. It isn't documented in the OpenAPI docs and
// is not shown in any of the example inputs.
//
// On that basis, I removed that test case. This implementation still does
// correctly handle variable type/attribute splits ("x", "y.z" / "x.y", "z")
// since that was required by the original "flattened" event representation.
rawEvents := expandEvents(tc.events)
for i, tc := range testCases {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
c, err := query.New(tc.s)
if err != nil {
t.Fatalf("NewCompiled %#q: unexpected error: %v", tc.s, err)
}
if tc.matches {
match, err := q.Matches(rawEvents)
require.Nil(t, err, "Query '%s' should not error on match %v", tc.s, tc.events)
require.True(t, match, "Query '%s' should match %v", tc.s, tc.events)
} else {
match, err := q.Matches(rawEvents)
require.Equal(t, tc.matchErr, err != nil, "Unexpected error for query '%s' match %v", tc.s, tc.events)
require.False(t, match, "Query '%s' should not match %v", tc.s, tc.events)
got, err := c.Matches(tc.events)
if err != nil {
t.Errorf("Query: %#q\nInput: %+v\nMatches: got error %v",
tc.s, tc.events, err)
}
if got != tc.matches {
t.Errorf("Query: %#q\nInput: %+v\nMatches: got %v, want %v",
tc.s, tc.events, got, tc.matches)
}
})
}
}
func TestAllMatchesAll(t *testing.T) {
events := newTestEvents(
``,
`Asher|Roth=`,
`Route|66=`,
`Rilly|Blue=`,
)
for i := 0; i < len(events); i++ {
match, err := query.All.Matches(events[:i])
if err != nil {
t.Errorf("Matches failed: %v", err)
} else if !match {
t.Errorf("Did not match on %+v ", events[:i])
}
}
}
func TestMustParse(t *testing.T) {
require.Panics(t, func() { query.MustParse("=") })
require.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") })
// newTestEvent constructs an Event message from a template string.
// The format is "type|attr1=val1|attr2=val2|...".
func newTestEvent(s string) types.Event {
var event types.Event
parts := strings.Split(s, "|")
event.Type = parts[0]
if len(parts) == 1 {
return event // type only, no attributes
}
for _, kv := range parts[1:] {
key, val := splitKV(kv)
event.Attributes = append(event.Attributes, types.EventAttribute{
Key: key,
Value: val,
})
}
return event
}
func TestConditions(t *testing.T) {
txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z")
require.NoError(t, err)
testCases := []struct {
s string
conditions []query.Condition
}{
{
s: "tm.events.type='NewBlock'",
conditions: []query.Condition{
{CompositeKey: "tm.events.type", Op: query.OpEqual, Operand: "NewBlock"},
},
},
{
s: "tx.gas > 7 AND tx.gas < 9",
conditions: []query.Condition{
{CompositeKey: "tx.gas", Op: query.OpGreater, Operand: int64(7)},
{CompositeKey: "tx.gas", Op: query.OpLess, Operand: int64(9)},
},
},
{
s: "tx.time >= TIME 2013-05-03T14:45:00Z",
conditions: []query.Condition{
{CompositeKey: "tx.time", Op: query.OpGreaterEqual, Operand: txTime},
},
},
{
s: "slashing EXISTS",
conditions: []query.Condition{
{CompositeKey: "slashing", Op: query.OpExists},
},
},
}
for _, tc := range testCases {
q, err := query.New(tc.s)
require.Nil(t, err)
c, err := q.Conditions()
require.NoError(t, err)
require.Equal(t, tc.conditions, c)
// newTestEvents constructs a slice of Event messages by applying newTestEvent
// to each element of ss.
func newTestEvents(ss ...string) []types.Event {
events := make([]types.Event, len(ss))
for i, s := range ss {
events[i] = newTestEvent(s)
}
return events
}
func splitKV(s string) (key, value string) {
kv := strings.SplitN(s, "=", 2)
return kv[0], kv[1]
}
+34
View File
@@ -0,0 +1,34 @@
// Package syntax defines a scanner and parser for the Tendermint event filter
// query language. A query selects events by their types and attribute values.
//
// Grammar
//
// The grammar of the query language is defined by the following EBNF:
//
// query = conditions EOF
// conditions = condition {"AND" condition}
// condition = tag comparison
// comparison = equal / order / contains / "EXISTS"
// equal = "=" (date / number / time / value)
// order = cmp (date / number / time)
// contains = "CONTAINS" value
// cmp = "<" / "<=" / ">" / ">="
//
// The lexical terms are defined here using RE2 regular expression notation:
//
// // The name of an event attribute (type.value)
// tag = #'\w+(\.\w+)*'
//
// // A datestamp (YYYY-MM-DD)
// date = #'DATE \d{4}-\d{2}-\d{2}'
//
// // A number with optional fractional parts (0, 10, 3.25)
// number = #'\d+(\.\d+)?'
//
// // An RFC3339 timestamp (2021-11-23T22:04:19-09:00)
// time = #'TIME \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([-+]\d{2}:\d{2}|Z)'
//
// // A quoted literal string value ('a b c')
// value = #'\'[^\']*\''
//
package syntax
+213
View File
@@ -0,0 +1,213 @@
package syntax
import (
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
)
// Parse parses the specified query string. It is shorthand for constructing a
// parser for s and calling its Parse method.
func Parse(s string) (Query, error) {
return NewParser(strings.NewReader(s)).Parse()
}
// Query is the root of the parse tree for a query. A query is the conjunction
// of one or more conditions.
type Query []Condition
func (q Query) String() string {
ss := make([]string, len(q))
for i, cond := range q {
ss[i] = cond.String()
}
return strings.Join(ss, " AND ")
}
// A Condition is a single conditional expression, consisting of a tag, a
// comparison operator, and an optional argument. The type of the argument
// depends on the operator.
type Condition struct {
Tag string
Op Token
Arg *Arg
opText string
}
func (c Condition) String() string {
s := c.Tag + " " + c.opText
if c.Arg != nil {
return s + " " + c.Arg.String()
}
return s
}
// An Arg is the argument of a comparison operator.
type Arg struct {
Type Token
text string
}
func (a *Arg) String() string {
if a == nil {
return ""
}
switch a.Type {
case TString:
return "'" + a.text + "'"
case TTime:
return "TIME " + a.text
case TDate:
return "DATE " + a.text
default:
return a.text
}
}
// Number returns the value of the argument text as a number, or a NaN if the
// text does not encode a valid number value.
func (a *Arg) Number() float64 {
if a == nil {
return -1
}
v, err := strconv.ParseFloat(a.text, 64)
if err == nil && v >= 0 {
return v
}
return math.NaN()
}
// Time returns the value of the argument text as a time, or the zero value if
// the text does not encode a timestamp or datestamp.
func (a *Arg) Time() time.Time {
var ts time.Time
if a == nil {
return ts
}
var err error
switch a.Type {
case TDate:
ts, err = ParseDate(a.text)
case TTime:
ts, err = ParseTime(a.text)
}
if err == nil {
return ts
}
return time.Time{}
}
// Value returns the value of the argument text as a string, or "".
func (a *Arg) Value() string {
if a == nil {
return ""
}
return a.text
}
// Parser is a query expression parser. The grammar for query expressions is
// defined in the syntax package documentation.
type Parser struct {
scanner *Scanner
}
// NewParser constructs a new parser that reads the input from r.
func NewParser(r io.Reader) *Parser {
return &Parser{scanner: NewScanner(r)}
}
// Parse parses the complete input and returns the resulting query.
func (p *Parser) Parse() (Query, error) {
cond, err := p.parseCond()
if err != nil {
return nil, err
}
conds := []Condition{cond}
for p.scanner.Next() != io.EOF {
if tok := p.scanner.Token(); tok != TAnd {
return nil, fmt.Errorf("offset %d: got %v, want %v", p.scanner.Pos(), tok, TAnd)
}
cond, err := p.parseCond()
if err != nil {
return nil, err
}
conds = append(conds, cond)
}
return conds, nil
}
// parseCond parses a conditional expression: tag OP value.
func (p *Parser) parseCond() (Condition, error) {
var cond Condition
if err := p.require(TTag); err != nil {
return cond, err
}
cond.Tag = p.scanner.Text()
if err := p.require(TLeq, TGeq, TLt, TGt, TEq, TContains, TExists); err != nil {
return cond, err
}
cond.Op = p.scanner.Token()
cond.opText = p.scanner.Text()
var err error
switch cond.Op {
case TLeq, TGeq, TLt, TGt:
err = p.require(TNumber, TTime, TDate)
case TEq:
err = p.require(TNumber, TTime, TDate, TString)
case TContains:
err = p.require(TString)
case TExists:
// no argument
return cond, nil
default:
return cond, fmt.Errorf("offset %d: unexpected operator %v", p.scanner.Pos(), cond.Op)
}
if err != nil {
return cond, err
}
cond.Arg = &Arg{Type: p.scanner.Token(), text: p.scanner.Text()}
return cond, nil
}
// require advances the scanner and requires that the resulting token is one of
// the specified token types.
func (p *Parser) require(tokens ...Token) error {
if err := p.scanner.Next(); err != nil {
return fmt.Errorf("offset %d: %w", p.scanner.Pos(), err)
}
got := p.scanner.Token()
for _, tok := range tokens {
if tok == got {
return nil
}
}
return fmt.Errorf("offset %d: got %v, wanted %s", p.scanner.Pos(), got, tokLabel(tokens))
}
// tokLabel makes a human-readable summary string for the given token types.
func tokLabel(tokens []Token) string {
if len(tokens) == 1 {
return tokens[0].String()
}
last := len(tokens) - 1
ss := make([]string, len(tokens)-1)
for i, tok := range tokens[:last] {
ss[i] = tok.String()
}
return strings.Join(ss, ", ") + " or " + tokens[last].String()
}
// ParseDate parses s as a date string in the format used by DATE values.
func ParseDate(s string) (time.Time, error) {
return time.Parse("2006-01-02", s)
}
// ParseTime parses s as a timestamp in the format used by TIME values.
func ParseTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
+312
View File
@@ -0,0 +1,312 @@
package syntax
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"time"
"unicode"
)
// Token is the type of a lexical token in the query grammar.
type Token byte
const (
TInvalid = iota // invalid or unknown token
TTag // field tag: x.y
TString // string value: 'foo bar'
TNumber // number: 0, 15.5, 100
TTime // timestamp: TIME yyyy-mm-ddThh:mm:ss([-+]hh:mm|Z)
TDate // datestamp: DATE yyyy-mm-dd
TAnd // operator: AND
TContains // operator: CONTAINS
TExists // operator: EXISTS
TEq // operator: =
TLt // operator: <
TLeq // operator: <=
TGt // operator: >
TGeq // operator: >=
// Do not reorder these values without updating the scanner code.
)
var tString = [...]string{
TInvalid: "invalid token",
TTag: "tag",
TString: "string",
TNumber: "number",
TTime: "timestamp",
TDate: "datestamp",
TAnd: "AND operator",
TContains: "CONTAINS operator",
TExists: "EXISTS operator",
TEq: "= operator",
TLt: "< operator",
TLeq: "<= operator",
TGt: "> operator",
TGeq: ">= operator",
}
func (t Token) String() string {
v := int(t)
if v > len(tString) {
return "unknown token type"
}
return tString[v]
}
const (
// TimeFormat is the format string used for timestamp values.
TimeFormat = time.RFC3339
// DateFormat is the format string used for datestamp values.
DateFormat = "2006-01-02"
)
// Scanner reads lexical tokens of the query language from an input stream.
// Each call to Next advances the scanner to the next token, or reports an
// error.
type Scanner struct {
r *bufio.Reader
buf bytes.Buffer
tok Token
err error
pos, last, end int
}
// NewScanner constructs a new scanner that reads from r.
func NewScanner(r io.Reader) *Scanner { return &Scanner{r: bufio.NewReader(r)} }
// Next advances s to the next token in the input, or reports an error. At the
// end of input, Next returns io.EOF.
func (s *Scanner) Next() error {
s.buf.Reset()
s.pos = s.end
s.tok = TInvalid
s.err = nil
for {
ch, err := s.rune()
if err != nil {
return s.fail(err)
}
if unicode.IsSpace(ch) {
s.pos = s.end
continue // skip whitespace
}
if '0' <= ch && ch <= '9' {
return s.scanNumber(ch)
} else if isTagRune(ch) {
return s.scanTagLike(ch)
}
switch ch {
case '\'':
return s.scanString(ch)
case '<', '>', '=':
return s.scanCompare(ch)
default:
return s.invalid(ch)
}
}
}
// Token returns the type of the current input token.
func (s *Scanner) Token() Token { return s.tok }
// Text returns the text of the current input token.
func (s *Scanner) Text() string { return s.buf.String() }
// Pos returns the start offset of the current token in the input.
func (s *Scanner) Pos() int { return s.pos }
// Err returns the last error reported by Next, if any.
func (s *Scanner) Err() error { return s.err }
// scanNumber scans for numbers with optional fractional parts.
// Examples: 0, 1, 3.14
func (s *Scanner) scanNumber(first rune) error {
s.buf.WriteRune(first)
if err := s.scanWhile(isDigit); err != nil {
return err
}
ch, err := s.rune()
if err != nil && err != io.EOF {
return err
}
if ch == '.' {
s.buf.WriteRune(ch)
if err := s.scanWhile(isDigit); err != nil {
return err
}
} else {
s.unrune()
}
s.tok = TNumber
return nil
}
func (s *Scanner) scanString(first rune) error {
// discard opening quote
for {
ch, err := s.rune()
if err != nil {
return s.fail(err)
} else if ch == first {
// discard closing quote
s.tok = TString
return nil
}
s.buf.WriteRune(ch)
}
}
func (s *Scanner) scanCompare(first rune) error {
s.buf.WriteRune(first)
switch first {
case '=':
s.tok = TEq
return nil
case '<':
s.tok = TLt
case '>':
s.tok = TGt
default:
return s.invalid(first)
}
ch, err := s.rune()
if err == io.EOF {
return nil // the assigned token is correct
} else if err != nil {
return s.fail(err)
}
if ch == '=' {
s.buf.WriteRune(ch)
s.tok++ // depends on token order
return nil
}
s.unrune()
return nil
}
func (s *Scanner) scanTagLike(first rune) error {
s.buf.WriteRune(first)
var hasSpace bool
for {
ch, err := s.rune()
if err == io.EOF {
break
} else if err != nil {
return s.fail(err)
}
if !isTagRune(ch) {
hasSpace = ch == ' ' // to check for TIME, DATE
break
}
s.buf.WriteRune(ch)
}
text := s.buf.String()
switch text {
case "TIME":
if hasSpace {
return s.scanTimestamp()
}
s.tok = TTag
case "DATE":
if hasSpace {
return s.scanDatestamp()
}
s.tok = TTag
case "AND":
s.tok = TAnd
case "EXISTS":
s.tok = TExists
case "CONTAINS":
s.tok = TContains
default:
s.tok = TTag
}
s.unrune()
return nil
}
func (s *Scanner) scanTimestamp() error {
s.buf.Reset() // discard "TIME" label
if err := s.scanWhile(isTimeRune); err != nil {
return err
}
if ts, err := time.Parse(TimeFormat, s.buf.String()); err != nil {
return s.fail(fmt.Errorf("invalid TIME value: %w", err))
} else if y := ts.Year(); y < 1900 || y > 2999 {
return s.fail(fmt.Errorf("timestamp year %d out of range", ts.Year()))
}
s.tok = TTime
return nil
}
func (s *Scanner) scanDatestamp() error {
s.buf.Reset() // discard "DATE" label
if err := s.scanWhile(isDateRune); err != nil {
return err
}
if ts, err := time.Parse(DateFormat, s.buf.String()); err != nil {
return s.fail(fmt.Errorf("invalid DATE value: %w", err))
} else if y := ts.Year(); y < 1900 || y > 2999 {
return s.fail(fmt.Errorf("datestamp year %d out of range", ts.Year()))
}
s.tok = TDate
return nil
}
func (s *Scanner) scanWhile(ok func(rune) bool) error {
for {
ch, err := s.rune()
if err == io.EOF {
return nil
} else if err != nil {
return s.fail(err)
} else if !ok(ch) {
s.unrune()
return nil
}
s.buf.WriteRune(ch)
}
}
func (s *Scanner) rune() (rune, error) {
ch, nb, err := s.r.ReadRune()
s.last = nb
s.end += nb
return ch, err
}
func (s *Scanner) unrune() {
_ = s.r.UnreadRune()
s.end -= s.last
}
func (s *Scanner) fail(err error) error {
s.err = err
return err
}
func (s *Scanner) invalid(ch rune) error {
return s.fail(fmt.Errorf("invalid input %c at offset %d", ch, s.end))
}
func isDigit(r rune) bool { return '0' <= r && r <= '9' }
func isTagRune(r rune) bool {
return r == '.' || r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
func isTimeRune(r rune) bool {
return strings.ContainsRune("-T:+Z", r) || isDigit(r)
}
func isDateRune(r rune) bool { return isDigit(r) || r == '-' }
+190
View File
@@ -0,0 +1,190 @@
package syntax_test
import (
"io"
"reflect"
"strings"
"testing"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
)
func TestScanner(t *testing.T) {
tests := []struct {
input string
want []syntax.Token
}{
// Empty inputs
{"", nil},
{" ", nil},
{"\t\n ", nil},
// Numbers
{`0 123`, []syntax.Token{syntax.TNumber, syntax.TNumber}},
{`0.32 3.14`, []syntax.Token{syntax.TNumber, syntax.TNumber}},
// Tags
{`foo foo.bar`, []syntax.Token{syntax.TTag, syntax.TTag}},
// Strings (values)
{` '' x 'x' 'x y'`, []syntax.Token{syntax.TString, syntax.TTag, syntax.TString, syntax.TString}},
{` 'you are not your job' `, []syntax.Token{syntax.TString}},
// Comparison operators
{`< <= = > >=`, []syntax.Token{
syntax.TLt, syntax.TLeq, syntax.TEq, syntax.TGt, syntax.TGeq,
}},
// Mixed values of various kinds.
{`x AND y`, []syntax.Token{syntax.TTag, syntax.TAnd, syntax.TTag}},
{`x.y CONTAINS 'z'`, []syntax.Token{syntax.TTag, syntax.TContains, syntax.TString}},
{`foo EXISTS`, []syntax.Token{syntax.TTag, syntax.TExists}},
{`and AND`, []syntax.Token{syntax.TTag, syntax.TAnd}},
// Timestamp
{`TIME 2021-11-23T15:16:17Z`, []syntax.Token{syntax.TTime}},
// Datestamp
{`DATE 2021-11-23`, []syntax.Token{syntax.TDate}},
}
for _, test := range tests {
s := syntax.NewScanner(strings.NewReader(test.input))
var got []syntax.Token
for s.Next() == nil {
got = append(got, s.Token())
}
if err := s.Err(); err != io.EOF {
t.Errorf("Next: unexpected error: %v", err)
}
if !reflect.DeepEqual(got, test.want) {
t.Logf("Scanner input: %q", test.input)
t.Errorf("Wrong tokens:\ngot: %+v\nwant: %+v", got, test.want)
}
}
}
func TestScannerErrors(t *testing.T) {
tests := []struct {
input string
}{
{`'incomplete string`},
{`-23`},
{`&`},
{`DATE xyz-pdq`},
{`DATE xyzp-dq-zv`},
{`DATE 0000-00-00`},
{`DATE 0000-00-000`},
{`DATE 2021-01-99`},
{`TIME 2021-01-01T34:56:78Z`},
{`TIME 2021-01-99T14:56:08Z`},
{`TIME 2021-01-99T34:56:08`},
{`TIME 2021-01-99T34:56:11+3`},
}
for _, test := range tests {
s := syntax.NewScanner(strings.NewReader(test.input))
if err := s.Next(); err == nil {
t.Errorf("Next: got %v (%#q), want error", s.Token(), s.Text())
}
}
}
// These parser tests were copied from the original implementation of the query
// parser, and are preserved here as a compatibility check.
func TestParseValid(t *testing.T) {
tests := []struct {
input string
valid bool
}{
{"tm.events.type='NewBlock'", true},
{"tm.events.type = 'NewBlock'", true},
{"tm.events.name = ''", true},
{"tm.events.type='TIME'", true},
{"tm.events.type='DATE'", true},
{"tm.events.type='='", true},
{"tm.events.type='TIME", false},
{"tm.events.type=TIME'", false},
{"tm.events.type==", false},
{"tm.events.type=NewBlock", false},
{">==", false},
{"tm.events.type 'NewBlock' =", false},
{"tm.events.type>'NewBlock'", false},
{"", false},
{"=", false},
{"='NewBlock'", false},
{"tm.events.type=", false},
{"tm.events.typeNewBlock", false},
{"tm.events.type'NewBlock'", false},
{"'NewBlock'", false},
{"NewBlock", false},
{"", false},
{"tm.events.type='NewBlock' AND abci.account.name='Igor'", true},
{"tm.events.type='NewBlock' AND", false},
{"tm.events.type='NewBlock' AN", false},
{"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false},
{"AND tm.events.type='NewBlock' ", false},
{"abci.account.name CONTAINS 'Igor'", true},
{"tx.date > DATE 2013-05-03", true},
{"tx.date < DATE 2013-05-03", true},
{"tx.date <= DATE 2013-05-03", true},
{"tx.date >= DATE 2013-05-03", true},
{"tx.date >= DAT 2013-05-03", false},
{"tx.date <= DATE2013-05-03", false},
{"tx.date <= DATE -05-03", false},
{"tx.date >= DATE 20130503", false},
{"tx.date >= DATE 2013+01-03", false},
// incorrect year, month, day
{"tx.date >= DATE 0013-01-03", false},
{"tx.date >= DATE 2013-31-03", false},
{"tx.date >= DATE 2013-01-83", false},
{"tx.date > TIME 2013-05-03T14:45:00+07:00", true},
{"tx.date < TIME 2013-05-03T14:45:00-02:00", true},
{"tx.date <= TIME 2013-05-03T14:45:00Z", true},
{"tx.date >= TIME 2013-05-03T14:45:00Z", true},
{"tx.date >= TIME2013-05-03T14:45:00Z", false},
{"tx.date = IME 2013-05-03T14:45:00Z", false},
{"tx.date = TIME 2013-05-:45:00Z", false},
{"tx.date >= TIME 2013-05-03T14:45:00", false},
{"tx.date >= TIME 0013-00-00T14:45:00Z", false},
{"tx.date >= TIME 2013+05=03T14:45:00Z", false},
{"account.balance=100", true},
{"account.balance >= 200", true},
{"account.balance >= -300", false},
{"account.balance >>= 400", false},
{"account.balance=33.22.1", false},
{"slashing.amount EXISTS", true},
{"slashing.amount EXISTS AND account.balance=100", true},
{"account.balance=100 AND slashing.amount EXISTS", true},
{"slashing EXISTS", true},
{"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true},
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false},
}
for _, test := range tests {
q, err := syntax.Parse(test.input)
if test.valid != (err == nil) {
t.Errorf("Parse %#q: valid %v got err=%v", test.input, test.valid, err)
}
// For valid queries, check that the query round-trips.
if test.valid {
qstr := q.String()
r, err := syntax.Parse(qstr)
if err != nil {
t.Errorf("Reparse %#q failed: %v", qstr, err)
}
if rstr := r.String(); rstr != qstr {
t.Errorf("Reparse diff\nold: %#q\nnew: %#q", qstr, rstr)
}
}
}
}
+22 -21
View File
@@ -136,17 +136,31 @@ func (bs *BaseService) Start(ctx context.Context) error {
}
go func(ctx context.Context) {
<-ctx.Done()
if err := bs.Stop(); err != nil {
bs.Logger.Error("stopped service",
"err", err.Error(),
select {
case <-bs.quit:
// someone else explicitly called stop
// and then we shouldn't.
return
case <-ctx.Done():
// if nothing is running, no need to
// shut down again.
if !bs.impl.IsRunning() {
return
}
// the context was cancel and we
// should stop.
if err := bs.Stop(); err != nil {
bs.Logger.Error("stopped service",
"err", err.Error(),
"service", bs.name,
"impl", bs.impl.String())
}
bs.Logger.Info("stopped service",
"service", bs.name,
"impl", bs.impl.String())
}
bs.Logger.Info("stopped service",
"service", bs.name,
"impl", bs.impl.String())
}(ctx)
return nil
@@ -156,11 +170,6 @@ func (bs *BaseService) Start(ctx context.Context) error {
return ErrAlreadyStarted
}
// OnStart implements Service by doing nothing.
// NOTE: Do not put anything in here,
// that way users don't need to call BaseService.OnStart()
func (bs *BaseService) OnStart(ctx context.Context) error { return nil }
// Stop implements Service by calling OnStop (if defined) and closing quit
// channel. An error will be returned if the service is already stopped.
func (bs *BaseService) Stop() error {
@@ -182,11 +191,6 @@ func (bs *BaseService) Stop() error {
return ErrAlreadyStopped
}
// OnStop implements Service by doing nothing.
// NOTE: Do not put anything in here,
// that way users don't need to call BaseService.OnStop()
func (bs *BaseService) OnStop() {}
// IsRunning implements Service by returning true or false depending on the
// service's state.
func (bs *BaseService) IsRunning() bool {
@@ -198,6 +202,3 @@ func (bs *BaseService) Wait() { <-bs.quit }
// String implements Service by returning a string representation of the service.
func (bs *BaseService) String() string { return bs.name }
// Quit Implements Service by returning a quit channel.
func (bs *BaseService) Quit() <-chan struct{} { return bs.quit }
+3 -2
View File
@@ -12,7 +12,8 @@ type testService struct {
BaseService
}
func (testService) OnReset() error {
func (testService) OnStop() {}
func (testService) OnStart(context.Context) error {
return nil
}
@@ -31,7 +32,7 @@ func TestBaseServiceWait(t *testing.T) {
waitFinished <- struct{}{}
}()
go ts.Stop() //nolint:errcheck // ignore for tests
go cancel()
select {
case <-waitFinished:
+1 -1
View File
@@ -77,7 +77,7 @@ func makeNetInfoFunc(c *lrpc.Client) rpcNetInfoFunc {
}
}
type rpcBlockchainInfoFunc func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) //nolint:lll
type rpcBlockchainInfoFunc func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error)
func makeBlockchainInfoFunc(c *lrpc.Client) rpcBlockchainInfoFunc {
return func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) {
+15 -6
View File
@@ -47,6 +47,8 @@ type Client struct {
// proof runtime used to verify values returned by ABCIQuery
prt *merkle.ProofRuntime
keyPathFn KeyPathFunc
quitCh chan struct{}
}
var _ rpcclient.Client = (*Client)(nil)
@@ -87,9 +89,10 @@ func DefaultMerkleKeyPathFn() KeyPathFunc {
// NewClient returns a new client.
func NewClient(next rpcclient.Client, lc LightClient, opts ...Option) *Client {
c := &Client{
next: next,
lc: lc,
prt: merkle.DefaultProofRuntime(),
next: next,
lc: lc,
prt: merkle.DefaultProofRuntime(),
quitCh: make(chan struct{}),
}
c.BaseService = *service.NewBaseService(nil, "Client", c)
for _, o := range opts {
@@ -102,6 +105,12 @@ func (c *Client) OnStart(ctx context.Context) error {
if !c.next.IsRunning() {
return c.next.Start(ctx)
}
go func() {
defer close(c.quitCh)
c.Wait()
}()
return nil
}
@@ -122,7 +131,7 @@ func (c *Client) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error
}
// ABCIQuery requests proof by default.
func (c *Client) ABCIQuery(ctx context.Context, path string, data tmbytes.HexBytes) (*coretypes.ResultABCIQuery, error) { //nolint:lll
func (c *Client) ABCIQuery(ctx context.Context, path string, data tmbytes.HexBytes) (*coretypes.ResultABCIQuery, error) {
return c.ABCIQueryWithOptions(ctx, path, data, rpcclient.DefaultABCIQueryOptions)
}
@@ -263,7 +272,7 @@ func (c *Client) Health(ctx context.Context) (*coretypes.ResultHealth, error) {
// BlockchainInfo calls rpcclient#BlockchainInfo and then verifies every header
// returned.
func (c *Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { //nolint:lll
func (c *Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) {
res, err := c.next.BlockchainInfo(ctx, minHeight, maxHeight)
if err != nil {
return nil, err
@@ -586,7 +595,7 @@ func (c *Client) SubscribeWS(ctx *rpctypes.Context, query string) (*coretypes.Re
rpctypes.JSONRPCStringID(fmt.Sprintf("%v#event", ctx.JSONReq.ID)),
resultEvent,
))
case <-c.Quit():
case <-c.quitCh:
return
}
}
+4 -4
View File
@@ -313,7 +313,7 @@ func makeNode(
sm.BlockExecutorWithMetrics(nodeMetrics.state),
)
csReactor, csState, err := createConsensusReactor(
csReactor, csState, err := createConsensusReactor(ctx,
cfg, state, blockExec, blockStore, mp, evPool,
privValidator, nodeMetrics.consensus, stateSync || blockSync, eventBus,
peerManager, router, logger,
@@ -599,7 +599,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) error {
// At the beginning of the statesync start, we use the initialHeight as the event height
// because of the statesync doesn't have the concreate state height before fetched the snapshot.
d := types.EventDataStateSyncStatus{Complete: false, Height: state.InitialHeight}
if err := n.eventBus.PublishEventStateSyncStatus(d); err != nil {
if err := n.eventBus.PublishEventStateSyncStatus(ctx, d); err != nil {
n.eventBus.Logger.Error("failed to emit the statesync start event", "err", err)
}
@@ -619,7 +619,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) error {
n.consensusReactor.SetStateSyncingMetrics(0)
if err := n.eventBus.PublishEventStateSyncStatus(
if err := n.eventBus.PublishEventStateSyncStatus(ctx,
types.EventDataStateSyncStatus{
Complete: true,
Height: state.LastBlockHeight,
@@ -638,7 +638,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) error {
return
}
if err := n.eventBus.PublishEventBlockSyncStatus(
if err := n.eventBus.PublishEventBlockSyncStatus(ctx,
types.EventDataBlockSyncStatus{
Complete: false,
Height: state.LastBlockHeight,
+21 -65
View File
@@ -48,45 +48,34 @@ func TestNodeStartStop(t *testing.T) {
// create & start node
ns, err := newDefaultNode(ctx, cfg, log.TestingLogger())
require.NoError(t, err)
require.NoError(t, ns.Start(ctx))
t.Cleanup(func() {
if ns.IsRunning() {
bcancel()
ns.Wait()
}
})
n, ok := ns.(*nodeImpl)
require.True(t, ok)
t.Cleanup(func() {
if n.IsRunning() {
bcancel()
n.Wait()
}
})
require.NoError(t, n.Start(ctx))
// wait for the node to produce a block
blocksSub, err := n.EventBus().SubscribeWithArgs(ctx, pubsub.SubscribeArgs{
tctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
blocksSub, err := n.EventBus().SubscribeWithArgs(tctx, pubsub.SubscribeArgs{
ClientID: "node_test",
Query: types.EventQueryNewBlock,
})
require.NoError(t, err)
tctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if _, err := blocksSub.Next(tctx); err != nil {
t.Fatalf("Waiting for event: %v", err)
}
_, err = blocksSub.Next(tctx)
require.NoError(t, err, "waiting for event")
// stop the node
go func() {
bcancel()
n.Wait()
}()
cancel() // stop the subscription context
bcancel() // stop the base context
n.Wait()
select {
case <-n.Quit():
return
case <-time.After(10 * time.Second):
if n.IsRunning() {
t.Fatal("timed out waiting for shutdown")
}
}
require.False(t, n.IsRunning(), "node must shut down")
}
func getTestNode(ctx context.Context, t *testing.T, conf *config.Config, logger log.Logger) *nodeImpl {
@@ -636,50 +625,17 @@ func TestNodeSetEventSink(t *testing.T) {
assert.Contains(t, err.Error(), "the psql connection settings cannot be empty")
t.Cleanup(cleanup(ns))
var psqlConn = "test"
cfg.TxIndex.Indexer = []string{"psql"}
cfg.TxIndex.PsqlConn = psqlConn
eventSinks = setupTest(t, cfg)
assert.Equal(t, 1, len(eventSinks))
assert.Equal(t, indexer.PSQL, eventSinks[0].Type())
cfg.TxIndex.Indexer = []string{"psql", "kv"}
cfg.TxIndex.PsqlConn = psqlConn
eventSinks = setupTest(t, cfg)
assert.Equal(t, 2, len(eventSinks))
// we use map to filter the duplicated sinks, so it's not guarantee the order when append sinks.
if eventSinks[0].Type() == indexer.KV {
assert.Equal(t, indexer.PSQL, eventSinks[1].Type())
} else {
assert.Equal(t, indexer.PSQL, eventSinks[0].Type())
assert.Equal(t, indexer.KV, eventSinks[1].Type())
}
cfg.TxIndex.Indexer = []string{"kv", "psql"}
cfg.TxIndex.PsqlConn = psqlConn
eventSinks = setupTest(t, cfg)
assert.Equal(t, 2, len(eventSinks))
if eventSinks[0].Type() == indexer.KV {
assert.Equal(t, indexer.PSQL, eventSinks[1].Type())
} else {
assert.Equal(t, indexer.PSQL, eventSinks[0].Type())
assert.Equal(t, indexer.KV, eventSinks[1].Type())
}
// N.B. We can't create a PSQL event sink without starting a postgres
// instance for it to talk to. The indexer service tests exercise that case.
var e = errors.New("found duplicated sinks, please check the tx-index section in the config.toml")
cfg.TxIndex.Indexer = []string{"psql", "kv", "Kv"}
cfg.TxIndex.PsqlConn = psqlConn
cfg.TxIndex.Indexer = []string{"null", "kv", "Kv"}
ns, err = newDefaultNode(ctx, cfg, logger)
require.Error(t, err)
assert.Contains(t, err.Error(), e.Error())
t.Cleanup(cleanup(ns))
cfg.TxIndex.Indexer = []string{"Psql", "kV", "kv", "pSql"}
cfg.TxIndex.PsqlConn = psqlConn
cfg.TxIndex.Indexer = []string{"Null", "kV", "kv", "nUlL"}
ns, err = newDefaultNode(ctx, cfg, logger)
require.Error(t, err)
assert.Contains(t, err.Error(), e.Error())
+2 -1
View File
@@ -302,6 +302,7 @@ func createBlockchainReactor(
}
func createConsensusReactor(
ctx context.Context,
cfg *config.Config,
state sm.State,
blockExec *sm.BlockExecutor,
@@ -318,7 +319,7 @@ func createConsensusReactor(
) (*consensus.Reactor, *consensus.State, error) {
logger = logger.With("module", "consensus")
consensusState := consensus.NewState(
consensusState := consensus.NewState(ctx,
logger,
cfg.Consensus,
state.Copy(),
-1
View File
@@ -57,7 +57,6 @@ func exampleProposal() *types.Proposal {
}
}
// nolint:lll // ignore line length for tests
func TestPrivvalVectors(t *testing.T) {
pk := ed25519.GenPrivKeyFromSecret([]byte("it's a secret")).PubKey()
ppk, err := encoding.PubKeyToProto(pk)
+3 -3
View File
@@ -60,10 +60,10 @@ func getSignerTestCases(ctx context.Context, t *testing.T) []signerTestCase {
}
func TestSignerClose(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bctx, bcancel := context.WithCancel(context.Background())
defer bcancel()
for _, tc := range getSignerTestCases(ctx, t) {
for _, tc := range getSignerTestCases(bctx, t) {
t.Run(tc.name, func(t *testing.T) {
defer tc.closer()
+4
View File
@@ -1,6 +1,7 @@
package privval
import (
"context"
"time"
"github.com/tendermint/tendermint/libs/log"
@@ -69,6 +70,9 @@ func NewSignerDialerEndpoint(
return sd
}
func (sd *SignerDialerEndpoint) OnStart(context.Context) error { return nil }
func (sd *SignerDialerEndpoint) OnStop() {}
func (sd *SignerDialerEndpoint) ensureConnection() error {
if sd.IsConnected() {
return nil
+7 -7
View File
@@ -72,8 +72,8 @@ func (sl *SignerListenerEndpoint) OnStart(ctx context.Context) error {
sl.pingInterval = time.Duration(sl.signerEndpoint.timeoutReadWrite.Milliseconds()*2/3) * time.Millisecond
sl.pingTimer = time.NewTicker(sl.pingInterval)
go sl.serviceLoop()
go sl.pingLoop()
go sl.serviceLoop(ctx)
go sl.pingLoop(ctx)
sl.connectRequestCh <- struct{}{}
@@ -173,7 +173,7 @@ func (sl *SignerListenerEndpoint) triggerReconnect() {
sl.triggerConnect()
}
func (sl *SignerListenerEndpoint) serviceLoop() {
func (sl *SignerListenerEndpoint) serviceLoop(ctx context.Context) {
for {
select {
case <-sl.connectRequestCh:
@@ -185,7 +185,7 @@ func (sl *SignerListenerEndpoint) serviceLoop() {
// We have a good connection, wait for someone that needs one otherwise cancellation
select {
case sl.connectionAvailableCh <- conn:
case <-sl.Quit():
case <-ctx.Done():
return
}
}
@@ -195,13 +195,13 @@ func (sl *SignerListenerEndpoint) serviceLoop() {
default:
}
}
case <-sl.Quit():
case <-ctx.Done():
return
}
}
}
func (sl *SignerListenerEndpoint) pingLoop() {
func (sl *SignerListenerEndpoint) pingLoop(ctx context.Context) {
for {
select {
case <-sl.pingTimer.C:
@@ -212,7 +212,7 @@ func (sl *SignerListenerEndpoint) pingLoop() {
sl.triggerReconnect()
}
}
case <-sl.Quit():
case <-ctx.Done():
return
}
}
+1 -5
View File
@@ -77,11 +77,7 @@ func TestSignerRemoteRetryTCPOnly(t *testing.T) {
err = signerServer.Start(ctx)
require.NoError(t, err)
t.Cleanup(func() {
if err := signerServer.Stop(); err != nil {
t.Error(err)
}
})
t.Cleanup(signerServer.Wait)
select {
case attempts := <-attemptCh:
-2
View File
@@ -94,8 +94,6 @@ func (ss *SignerServer) servicePendingRequest() {
func (ss *SignerServer) serviceLoop(ctx context.Context) {
for {
select {
case <-ss.Quit():
return
case <-ctx.Done():
return
default:
@@ -84,7 +84,6 @@ func TestStatusResponse_Validate(t *testing.T) {
}
}
// nolint:lll
func TestBlockchainMessageVectors(t *testing.T) {
block := types.MakeBlock(int64(3), []types.Tx{types.Tx("Hello World")}, nil, nil)
block.Version.Block = 11 // overwrite updated protocol version
+1 -1
View File
@@ -134,7 +134,7 @@ type EventsClient interface {
//
// ctx cannot be used to unsubscribe. To unsubscribe, use either Unsubscribe
// or UnsubscribeAll.
Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan coretypes.ResultEvent, err error) //nolint:lll
Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan coretypes.ResultEvent, err error)
// Unsubscribe unsubscribes given subscriber from query.
Unsubscribe(ctx context.Context, subscriber, query string) error
// UnsubscribeAll unsubscribes given subscriber from all the queries.

Some files were not shown because too many files have changed in this diff Show More