mirror of
https://github.com/tendermint/tendermint.git
synced 2026-06-10 00:03:04 +00:00
Merge branch 'master' into wb/tm-signer-harness
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Generated
+3
-3
@@ -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
@@ -7,7 +7,7 @@
|
||||
"vuepress-theme-cosmos": "^1.0.182"
|
||||
},
|
||||
"devDependencies": {
|
||||
"watchpack": "^2.2.0"
|
||||
"watchpack": "^2.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preserve": "./pre.sh",
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package query
|
||||
|
||||
//go:generate peg -inline -switch query.peg
|
||||
+280
-480
@@ -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))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 == '-' }
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user